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

mchades 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 b354e8fcbe [#13786] feat(core): Add Semantic Model import/export 
events (#13791)
b354e8fcbe is described below

commit b354e8fcbe77e730643ffa30529ee80bd6ed88a1
Author: mchades <[email protected]>
AuthorDate: Wed Oct 14 16:54:59 2026 +0800

    [#13786] feat(core): Add Semantic Model import/export events (#13791)
    
    ### What changes were proposed in this pull request?
    Add dedicated import/export events and audit support, consolidate import
    dispatching, and preserve source validation and ownership.
    
    ### Why are the changes needed?
    Treat Ossie import/export as distinct operations without additional
    create/load events.
    
    Fix: #13786
    
    ### Does this PR introduce _any_ user-facing change?
    Listeners and audit logs now distinguish import/export operations. No
    REST or client API changes.
    
    ### How was this patch tested?
    Core and Server unit tests, formatting, and Javadoc checks.
---
 .../java/org/apache/gravitino/audit/AuditLog.java  |  14 +
 .../gravitino/audit/v2/CompatibilityUtils.java     |   2 +
 .../gravitino/catalog/SemanticModelDispatcher.java | 154 +++++++-
 .../catalog/SemanticModelNormalizeDispatcher.java  |  17 +
 .../catalog/SemanticModelOperationDispatcher.java  |  18 +
 .../hook/SemanticModelHookDispatcher.java          |  39 +-
 .../listener/SemanticModelEventDispatcher.java     |  57 +++
 .../listener/api/event/OperationType.java          |   4 +
 .../event/semantic/ExportSemanticModelEvent.java   |  57 +++
 .../semantic/ExportSemanticModelFailureEvent.java  |  61 +++
 .../semantic/ExportSemanticModelPreEvent.java      |  60 +++
 .../event/semantic/ImportSemanticModelEvent.java   |  74 ++++
 .../semantic/ImportSemanticModelFailureEvent.java  |  73 ++++
 .../semantic/ImportSemanticModelPreEvent.java      |  72 ++++
 .../gravitino/semantic/OssieDocumentConverter.java |  19 +-
 .../gravitino/audit/v2/TestCompatibilityUtils.java |   2 +
 .../catalog/TestSemanticModelDispatcher.java       |  97 +++++
 .../TestSemanticModelNormalizeDispatcher.java      |  31 +-
 .../TestSemanticModelOperationDispatcher.java      |  64 +++-
 .../hook/TestSemanticModelHookDispatcher.java      |  30 ++
 .../event/TestSemanticModelImportExportEvent.java  | 415 +++++++++++++++++++++
 .../semantic/TestOssieDocumentConverter.java       |  30 +-
 .../server/web/rest/SemanticModelOperations.java   |  13 +-
 .../web/rest/TestSemanticModelOperations.java      | 175 +++++++--
 24 files changed, 1463 insertions(+), 115 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java 
b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
index acf532f24c..fc6d9ac5af 100644
--- a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
+++ b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
@@ -115,6 +115,10 @@ import 
org.apache.gravitino.listener.api.event.semantic.CreateSemanticModelEvent
 import 
org.apache.gravitino.listener.api.event.semantic.CreateSemanticModelFailureEvent;
 import org.apache.gravitino.listener.api.event.semantic.DropSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.DropSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelFailureEvent;
 import org.apache.gravitino.listener.api.event.semantic.ListSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.ListSemanticModelFailureEvent;
 import org.apache.gravitino.listener.api.event.semantic.LoadSemanticModelEvent;
@@ -527,6 +531,10 @@ public interface AuditLog {
     LOAD_SEMANTIC_MODEL,
 
     LIST_SEMANTIC_MODEL,
+    /** Import a Semantic Model from an Ossie document. */
+    IMPORT_SEMANTIC_MODEL,
+    /** Export a Semantic Model as an Ossie document. */
+    EXPORT_SEMANTIC_MODEL,
 
     REGISTER_FUNCTION,
 
@@ -647,6 +655,12 @@ public interface AuditLog {
       } else if (event instanceof ListSemanticModelEvent
           || event instanceof ListSemanticModelFailureEvent) {
         return LIST_SEMANTIC_MODEL;
+      } else if (event instanceof ImportSemanticModelEvent
+          || event instanceof ImportSemanticModelFailureEvent) {
+        return IMPORT_SEMANTIC_MODEL;
+      } else if (event instanceof ExportSemanticModelEvent
+          || event instanceof ExportSemanticModelFailureEvent) {
+        return EXPORT_SEMANTIC_MODEL;
       } else if (event instanceof CreateFilesetEvent
           || event instanceof CreateFilesetFailureEvent) {
         return CREATE_FILESET;
diff --git 
a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java 
b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
index bc220eff62..83ad070753 100644
--- a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
+++ b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
@@ -200,6 +200,8 @@ public class CompatibilityUtils {
           .put(OperationType.DROP_SEMANTIC_MODEL, 
Operation.DROP_SEMANTIC_MODEL)
           .put(OperationType.LOAD_SEMANTIC_MODEL, 
Operation.LOAD_SEMANTIC_MODEL)
           .put(OperationType.LIST_SEMANTIC_MODEL, 
Operation.LIST_SEMANTIC_MODEL)
+          .put(OperationType.IMPORT_SEMANTIC_MODEL, 
Operation.IMPORT_SEMANTIC_MODEL)
+          .put(OperationType.EXPORT_SEMANTIC_MODEL, 
Operation.EXPORT_SEMANTIC_MODEL)
           .put(OperationType.REGISTER_FUNCTION, Operation.REGISTER_FUNCTION)
           .put(OperationType.GET_FUNCTION, Operation.GET_FUNCTION)
           .put(OperationType.ALTER_FUNCTION, Operation.ALTER_FUNCTION)
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelDispatcher.java
index b2c0a122d1..6e3dee8adb 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelDispatcher.java
@@ -18,28 +18,100 @@
  */
 package org.apache.gravitino.catalog;
 
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
+import org.apache.gravitino.exceptions.IllegalSemanticModelException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
+import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
 import org.apache.gravitino.semantic.OssieDocument;
 import org.apache.gravitino.semantic.OssieDocumentConverter;
-import 
org.apache.gravitino.semantic.OssieDocumentConverter.ImportedSemanticModel;
+import 
org.apache.gravitino.semantic.OssieDocumentConverter.ParsedSemanticModel;
 import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
-import org.apache.gravitino.semantic.SemanticModelCatalog;
+import org.apache.gravitino.semantic.SemanticModelChange;
+import org.apache.gravitino.semantic.SemanticModelDefinition;
 
-/** A dispatcher specialization for schema-scoped Semantic Model operations. */
-public interface SemanticModelDispatcher extends SemanticModelCatalog {
+/** Dispatches schema-scoped Semantic Model operations through the server 
pipeline. */
+public interface SemanticModelDispatcher {
 
   /**
-   * {@inheritDoc}
+   * Lists the Semantic Models in a schema namespace.
    *
-   * <p>Delegates to this dispatcher's native create operation so that the 
document follows the same
-   * normalization and validation chain as a structured create request.
+   * @param namespace The schema namespace.
+   * @return The identifiers of the Semantic Models in the namespace.
+   * @throws NoSuchSchemaException If the schema does not exist.
    */
-  @Override
-  default SemanticModel importOssieDocument(Namespace namespace, OssieDocument 
document) {
-    ImportedSemanticModel model = 
OssieDocumentConverter.importDocument(document);
-    return createSemanticModel(
+  NameIdentifier[] listSemanticModels(Namespace namespace) throws 
NoSuchSchemaException;
+
+  /**
+   * Loads a Semantic Model by identifier.
+   *
+   * @param ident The Semantic Model identifier.
+   * @return The loaded Semantic Model.
+   * @throws NoSuchSemanticModelException If the Semantic Model does not exist.
+   */
+  SemanticModel loadSemanticModel(NameIdentifier ident) throws 
NoSuchSemanticModelException;
+
+  /**
+   * Returns whether a Semantic Model exists.
+   *
+   * @param ident The Semantic Model identifier.
+   * @return {@code true} if the Semantic Model exists, otherwise {@code 
false}.
+   */
+  default boolean semanticModelExists(NameIdentifier ident) {
+    try {
+      loadSemanticModel(ident);
+      return true;
+    } catch (NoSuchSemanticModelException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Creates a Semantic Model in a schema.
+   *
+   * @param ident The Semantic Model identifier.
+   * @param comment The Semantic Model comment, or {@code null} if it has no 
comment.
+   * @param definition The complete Semantic Model definition.
+   * @param properties The Gravitino-specific Semantic Model properties.
+   * @return The created Semantic Model.
+   * @throws NoSuchSchemaException If the schema does not exist.
+   * @throws SemanticModelAlreadyExistsException If the Semantic Model already 
exists.
+   * @throws IllegalSemanticModelException If the Semantic Model definition is 
invalid.
+   */
+  SemanticModel createSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties)
+      throws NoSuchSchemaException, SemanticModelAlreadyExistsException,
+          IllegalSemanticModelException;
+
+  /**
+   * Imports an Ossie document after validating its sources under the current 
caller.
+   *
+   * <p>The document is parsed once, and source validation runs before any 
model write. Keeping the
+   * validator caller-supplied avoids a dependency on server authorization in 
Core. Callers must
+   * validate source existence even when authorization is disabled.
+   *
+   * @param namespace The destination schema namespace.
+   * @param document The document to import.
+   * @param sourceValidator The required caller-specific source validator.
+   * @return The imported Semantic Model.
+   */
+  default SemanticModel importOssieDocument(
+      Namespace namespace,
+      OssieDocument document,
+      Consumer<SemanticModelDefinition> sourceValidator) {
+    Objects.requireNonNull(sourceValidator, "sourceValidator must not be 
null");
+    ParsedSemanticModel model = 
OssieDocumentConverter.importDocument(document);
+    sourceValidator.accept(model.definition());
+    return importSemanticModel(
         NameIdentifier.of(namespace, model.name()),
         model.comment(),
         model.definition(),
@@ -47,12 +119,60 @@ public interface SemanticModelDispatcher extends 
SemanticModelCatalog {
   }
 
   /**
-   * {@inheritDoc}
+   * Dispatches a parsed import through normalization and import-specific 
hooks.
+   *
+   * <p>This internal entry point is used by {@link 
#importOssieDocument(Namespace, OssieDocument,
+   * Consumer)} after parsing and source validation. Each decorator implements 
it separately from
+   * create; only the operation dispatcher reuses the underlying create 
implementation. The
+   * top-level event dispatcher rejects direct calls to this internal entry 
point.
    *
-   * <p>Loads the model through this dispatcher's native load operation before 
serializing it.
+   * @param ident The identifier of the model to import.
+   * @param comment The model comment, or {@code null} if it has no comment.
+   * @param definition The parsed native definition whose sources have been 
validated.
+   * @param properties The Gravitino-specific model properties.
+   * @return The imported Semantic Model.
+   * @throws UnsupportedOperationException If called directly on the top-level 
event dispatcher.
    */
-  @Override
-  default OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat 
format) {
-    return OssieDocumentConverter.exportDocument(loadSemanticModel(ident), 
format);
-  }
+  SemanticModel importSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties);
+
+  /**
+   * Exports a Semantic Model as a standalone Apache Ossie document.
+   *
+   * <p>Each decorator implements this operation separately from load; only 
the operation dispatcher
+   * reuses the underlying load implementation before serializing the model.
+   *
+   * @param ident The Semantic Model identifier.
+   * @param format The requested serialization format.
+   * @return The serialized Ossie document.
+   * @throws NoSuchSemanticModelException If the Semantic Model does not exist.
+   * @throws IllegalSemanticModelException If the model cannot be represented 
as an Ossie document.
+   */
+  OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat format);
+
+  /**
+   * Applies changes atomically to a Semantic Model.
+   *
+   * @param ident The Semantic Model identifier.
+   * @param changes The changes to apply.
+   * @return The altered Semantic Model.
+   * @throws NoSuchSemanticModelException If the Semantic Model does not exist.
+   * @throws SemanticModelAlreadyExistsException If a rename conflicts with an 
existing Semantic
+   *     Model.
+   * @throws IllegalSemanticModelException If a change or the resulting 
Semantic Model is invalid.
+   */
+  SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
+      throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
+          IllegalSemanticModelException;
+
+  /**
+   * Drops a Semantic Model.
+   *
+   * @param ident The Semantic Model identifier.
+   * @return {@code true} if the Semantic Model was dropped, or {@code false} 
if it did not exist.
+   */
+  boolean dropSemanticModel(NameIdentifier ident);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelNormalizeDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelNormalizeDispatcher.java
index ac6411db27..e9df9ba6da 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelNormalizeDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelNormalizeDispatcher.java
@@ -34,6 +34,8 @@ import 
org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
 import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
@@ -83,6 +85,21 @@ public class SemanticModelNormalizeDispatcher implements 
SemanticModelDispatcher
         normalizeForCreate(ident), comment, definition, properties);
   }
 
+  @Override
+  public SemanticModel importSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties) {
+    return dispatcher.importSemanticModel(
+        normalizeForCreate(ident), comment, definition, properties);
+  }
+
+  @Override
+  public OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat 
format) {
+    return dispatcher.exportOssieDocument(normalizeForLookup(ident), format);
+  }
+
   @Override
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
index 132a195830..07fe80c3f2 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
@@ -35,6 +35,9 @@ import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
 import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
 import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieDocumentConverter;
+import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
@@ -115,6 +118,21 @@ public class SemanticModelOperationDispatcher extends 
OperationDispatcher
     return managedOperations.createSemanticModel(ident, comment, definition, 
effectiveProperties);
   }
 
+  @Override
+  public SemanticModel importSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties) {
+    return createSemanticModel(ident, comment, definition, properties);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat 
format) {
+    return OssieDocumentConverter.exportDocument(loadSemanticModel(ident), 
format);
+  }
+
   @Override
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
diff --git 
a/core/src/main/java/org/apache/gravitino/hook/SemanticModelHookDispatcher.java 
b/core/src/main/java/org/apache/gravitino/hook/SemanticModelHookDispatcher.java
index 6e422152ae..f8f4249cf7 100644
--- 
a/core/src/main/java/org/apache/gravitino/hook/SemanticModelHookDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/hook/SemanticModelHookDispatcher.java
@@ -33,6 +33,8 @@ import 
org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
 import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
@@ -86,19 +88,27 @@ public class SemanticModelHookDispatcher implements 
SemanticModelDispatcher {
           IllegalSemanticModelException {
     SemanticModel semanticModel =
         dispatcher.createSemanticModel(ident, comment, definition, properties);
+    setCreatorAsOwner(ident);
+    return semanticModel;
+  }
 
-    // Set the creator as the owner of the Semantic Model.
-    OwnerDispatcher ownerManager = ownerDispatcher.get();
-    if (ownerManager != null) {
-      ownerManager.setOwner(
-          ident.namespace().level(0),
-          NameIdentifierUtil.toMetadataObject(ident, 
Entity.EntityType.SEMANTIC_MODEL),
-          PrincipalUtils.getCurrentUserName(),
-          Owner.Type.USER);
-    }
+  @Override
+  public SemanticModel importSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties) {
+    SemanticModel semanticModel =
+        dispatcher.importSemanticModel(ident, comment, definition, properties);
+    setCreatorAsOwner(ident);
     return semanticModel;
   }
 
+  @Override
+  public OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat 
format) {
+    return dispatcher.exportOssieDocument(ident, format);
+  }
+
   @Override
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
@@ -123,4 +133,15 @@ public class SemanticModelHookDispatcher implements 
SemanticModelDispatcher {
     }
     return dropped;
   }
+
+  private void setCreatorAsOwner(NameIdentifier ident) {
+    OwnerDispatcher ownerManager = ownerDispatcher.get();
+    if (ownerManager != null) {
+      ownerManager.setOwner(
+          ident.namespace().level(0),
+          NameIdentifierUtil.toMetadataObject(ident, 
Entity.EntityType.SEMANTIC_MODEL),
+          PrincipalUtils.getCurrentUserName(),
+          Owner.Type.USER);
+    }
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/SemanticModelEventDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/listener/SemanticModelEventDispatcher.java
index 8eca83aef9..fa4a344425 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/SemanticModelEventDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/SemanticModelEventDispatcher.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.listener;
 
 import java.util.Map;
+import java.util.function.Consumer;
 import javax.annotation.Nullable;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
@@ -38,6 +39,12 @@ import 
org.apache.gravitino.listener.api.event.semantic.CreateSemanticModelPreEv
 import org.apache.gravitino.listener.api.event.semantic.DropSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.DropSemanticModelFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.DropSemanticModelPreEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelPreEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelPreEvent;
 import org.apache.gravitino.listener.api.event.semantic.ListSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.ListSemanticModelFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.ListSemanticModelPreEvent;
@@ -45,6 +52,8 @@ import 
org.apache.gravitino.listener.api.event.semantic.LoadSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.LoadSemanticModelFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.LoadSemanticModelPreEvent;
 import org.apache.gravitino.listener.api.info.SemanticModelInfo;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
@@ -131,6 +140,54 @@ public class SemanticModelEventDispatcher implements 
SemanticModelDispatcher {
     }
   }
 
+  @Override
+  public SemanticModel importOssieDocument(
+      Namespace namespace,
+      OssieDocument document,
+      Consumer<SemanticModelDefinition> sourceValidator) {
+    String user = PrincipalUtils.getCurrentUserName();
+    OssieFormat format = document == null ? null : document.format();
+    try {
+      eventBus.dispatchEvent(new ImportSemanticModelPreEvent(user, namespace, 
format));
+      SemanticModel semanticModel =
+          dispatcher.importOssieDocument(namespace, document, sourceValidator);
+      eventBus.dispatchEvent(
+          new ImportSemanticModelEvent(
+              user,
+              NameIdentifier.of(namespace, semanticModel.name()),
+              format,
+              new SemanticModelInfo(semanticModel)));
+      return semanticModel;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new ImportSemanticModelFailureEvent(user, 
namespace, format, e));
+      throw e;
+    }
+  }
+
+  @Override
+  public SemanticModel importSemanticModel(
+      NameIdentifier ident,
+      @Nullable String comment,
+      SemanticModelDefinition definition,
+      Map<String, String> properties) {
+    throw new UnsupportedOperationException(
+        "Parsed imports are internal; use importOssieDocument with a source 
validator");
+  }
+
+  @Override
+  public OssieDocument exportOssieDocument(NameIdentifier ident, OssieFormat 
format) {
+    String user = PrincipalUtils.getCurrentUserName();
+    try {
+      eventBus.dispatchEvent(new ExportSemanticModelPreEvent(user, ident, 
format));
+      OssieDocument document = dispatcher.exportOssieDocument(ident, format);
+      eventBus.dispatchEvent(new ExportSemanticModelEvent(user, ident, 
format));
+      return document;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new ExportSemanticModelFailureEvent(user, ident, 
format, e));
+      throw e;
+    }
+  }
+
   @Override
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index 072dc00d42..e21c70fe34 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -215,6 +215,10 @@ public enum OperationType {
   DROP_SEMANTIC_MODEL,
   LOAD_SEMANTIC_MODEL,
   LIST_SEMANTIC_MODEL,
+  /** Import a Semantic Model from an Ossie document. */
+  IMPORT_SEMANTIC_MODEL,
+  /** Export a Semantic Model as an Ossie document. */
+  EXPORT_SEMANTIC_MODEL,
 
   // Function operations
   REGISTER_FUNCTION,
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelEvent.java
new file mode 100644
index 0000000000..5df4d49d4f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelEvent.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.listener.api.event.semantic;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Successful event for exporting a Semantic Model as an Ossie document. */
+@DeveloperApi
+public final class ExportSemanticModelEvent extends SemanticModelEvent {
+  private final OssieFormat format;
+
+  /**
+   * Constructs a {@code ExportSemanticModelEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param identifier The Semantic Model identifier.
+   * @param format The non-null document format.
+   */
+  public ExportSemanticModelEvent(String user, NameIdentifier identifier, 
OssieFormat format) {
+    super(user, identifier);
+    this.format = format;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The non-null document format.
+   */
+  public OssieFormat format() {
+    return format;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.EXPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelFailureEvent.java
new file mode 100644
index 0000000000..a44dc4c2a1
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelFailureEvent.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.gravitino.listener.api.event.semantic;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Failure event for exporting a Semantic Model as an Ossie document. */
+@DeveloperApi
+public final class ExportSemanticModelFailureEvent extends 
SemanticModelFailureEvent {
+  @Nullable private final OssieFormat format;
+
+  /**
+   * Constructs a {@code ExportSemanticModelFailureEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param identifier The Semantic Model identifier.
+   * @param format The document format, or null for an invalid request.
+   * @param exception The exception that caused the operation to fail.
+   */
+  public ExportSemanticModelFailureEvent(
+      String user, NameIdentifier identifier, @Nullable OssieFormat format, 
Exception exception) {
+    super(user, identifier, exception);
+    this.format = format;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The format, or null for an invalid request.
+   */
+  @Nullable
+  public OssieFormat format() {
+    return format;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.EXPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelPreEvent.java
new file mode 100644
index 0000000000..1c3f0afa7f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ExportSemanticModelPreEvent.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.gravitino.listener.api.event.semantic;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Pre-event before exporting a Semantic Model as an Ossie document. */
+@DeveloperApi
+public final class ExportSemanticModelPreEvent extends SemanticModelPreEvent {
+  @Nullable private final OssieFormat format;
+
+  /**
+   * Constructs a {@code ExportSemanticModelPreEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param identifier The Semantic Model identifier.
+   * @param format The document format, or null for an invalid request.
+   */
+  public ExportSemanticModelPreEvent(
+      String user, NameIdentifier identifier, @Nullable OssieFormat format) {
+    super(user, identifier);
+    this.format = format;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The format, or null for an invalid request.
+   */
+  @Nullable
+  public OssieFormat format() {
+    return format;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.EXPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelEvent.java
new file mode 100644
index 0000000000..81104d982b
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelEvent.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.gravitino.listener.api.event.semantic;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.listener.api.info.SemanticModelInfo;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Successful event for importing an Ossie document as a Semantic Model. */
+@DeveloperApi
+public final class ImportSemanticModelEvent extends SemanticModelEvent {
+  private final OssieFormat format;
+  private final SemanticModelInfo importedSemanticModelInfo;
+
+  /**
+   * Constructs a {@code ImportSemanticModelEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param identifier The Semantic Model identifier.
+   * @param format The non-null document format.
+   * @param importedSemanticModelInfo The imported Semantic Model information.
+   */
+  public ImportSemanticModelEvent(
+      String user,
+      NameIdentifier identifier,
+      OssieFormat format,
+      SemanticModelInfo importedSemanticModelInfo) {
+    super(user, identifier);
+    this.format = format;
+    this.importedSemanticModelInfo = importedSemanticModelInfo;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The non-null document format.
+   */
+  public OssieFormat format() {
+    return format;
+  }
+
+  /**
+   * Returns the imported Semantic Model information.
+   *
+   * @return The imported Semantic Model information.
+   */
+  public SemanticModelInfo importedSemanticModelInfo() {
+    return importedSemanticModelInfo;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.IMPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelFailureEvent.java
new file mode 100644
index 0000000000..7535666785
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelFailureEvent.java
@@ -0,0 +1,73 @@
+/*
+ * 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.listener.api.event.semantic;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Failure event for importing an Ossie document as a Semantic Model. */
+@DeveloperApi
+public final class ImportSemanticModelFailureEvent extends 
SemanticModelFailureEvent {
+  private final Namespace namespace;
+  @Nullable private final OssieFormat format;
+
+  /**
+   * Constructs a {@code ImportSemanticModelFailureEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param namespace The destination schema; the model name may not yet be 
available.
+   * @param format The document format, or null for an invalid request.
+   * @param exception The exception that caused the operation to fail.
+   */
+  public ImportSemanticModelFailureEvent(
+      String user, Namespace namespace, @Nullable OssieFormat format, 
Exception exception) {
+    super(user, NameIdentifier.of(namespace.levels()), exception);
+    this.namespace = namespace;
+    this.format = format;
+  }
+
+  /**
+   * Returns the destination schema namespace.
+   *
+   * @return The destination namespace.
+   */
+  public Namespace namespace() {
+    return namespace;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The format, or null for an invalid request.
+   */
+  @Nullable
+  public OssieFormat format() {
+    return format;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.IMPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelPreEvent.java
new file mode 100644
index 0000000000..5531bb2418
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/semantic/ImportSemanticModelPreEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.listener.api.event.semantic;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.semantic.OssieFormat;
+
+/** Pre-event before importing an Ossie document as a Semantic Model. */
+@DeveloperApi
+public final class ImportSemanticModelPreEvent extends SemanticModelPreEvent {
+  private final Namespace namespace;
+  @Nullable private final OssieFormat format;
+
+  /**
+   * Constructs a {@code ImportSemanticModelPreEvent}.
+   *
+   * @param user The username of the individual who initiated the operation.
+   * @param namespace The destination schema; the model name may not yet be 
available.
+   * @param format The document format, or null for an invalid request.
+   */
+  public ImportSemanticModelPreEvent(
+      String user, Namespace namespace, @Nullable OssieFormat format) {
+    super(user, NameIdentifier.of(namespace.levels()));
+    this.namespace = namespace;
+    this.format = format;
+  }
+
+  /**
+   * Returns the destination schema namespace.
+   *
+   * @return The destination namespace.
+   */
+  public Namespace namespace() {
+    return namespace;
+  }
+
+  /**
+   * Returns the document format.
+   *
+   * @return The format, or null for an invalid request.
+   */
+  @Nullable
+  public OssieFormat format() {
+    return format;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.IMPORT_SEMANTIC_MODEL;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/semantic/OssieDocumentConverter.java 
b/core/src/main/java/org/apache/gravitino/semantic/OssieDocumentConverter.java
index 08f80c9a66..b855e545d5 100644
--- 
a/core/src/main/java/org/apache/gravitino/semantic/OssieDocumentConverter.java
+++ 
b/core/src/main/java/org/apache/gravitino/semantic/OssieDocumentConverter.java
@@ -108,10 +108,10 @@ public final class OssieDocumentConverter {
    * @throws IllegalSemanticModelException If the document cannot be parsed or 
represented by
    *     Gravitino.
    */
-  public static ImportedSemanticModel importDocument(OssieDocument document) {
+  public static ParsedSemanticModel importDocument(OssieDocument document) {
     Objects.requireNonNull(document, "document must not be null");
     ObjectNode root = parseDocument(document);
-    return toImportedModel(root);
+    return toParsedModel(root);
   }
 
   /**
@@ -139,7 +139,7 @@ public final class OssieDocumentConverter {
     stashProperties(root, semanticModel.properties());
 
     // Validate the generated document through the same conversion path used 
for imports.
-    toImportedModel(root.deepCopy());
+    toParsedModel(root.deepCopy());
 
     try {
       if (format == OssieFormat.JSON) {
@@ -153,15 +153,18 @@ public final class OssieDocumentConverter {
     }
   }
 
-  /** Native model values parsed from an Ossie document, before the model is 
created. */
-  public static final class ImportedSemanticModel {
+  /**
+   * Immutable native model values parsed from an Ossie document. Parsing does 
not validate source
+   * existence or permissions, or create the model.
+   */
+  public static final class ParsedSemanticModel {
 
     private final String name;
     @Nullable private final String comment;
     private final SemanticModelDefinition definition;
     private final Map<String, String> properties;
 
-    private ImportedSemanticModel(
+    private ParsedSemanticModel(
         String name,
         @Nullable String comment,
         SemanticModelDefinition definition,
@@ -248,7 +251,7 @@ public final class OssieDocumentConverter {
     }
   }
 
-  private static ImportedSemanticModel toImportedModel(ObjectNode root) {
+  private static ParsedSemanticModel toParsedModel(ObjectNode root) {
     validateObject(root, "$", ROOT_PROPERTIES);
     String name = readText(root, "name", "$");
     String comment = readText(root, "description", "$");
@@ -267,7 +270,7 @@ public final class OssieDocumentConverter {
       if (StringUtils.isBlank(name)) {
         throw new IllegalArgumentException("\"name\" field is required and 
cannot be empty");
       }
-      return new ImportedSemanticModel(name, comment, readDefinition(root), 
properties);
+      return new ParsedSemanticModel(name, comment, readDefinition(root), 
properties);
     } catch (IllegalSemanticModelException e) {
       throw e;
     } catch (IllegalArgumentException e) {
diff --git 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
index fdfecbfb6b..6aa10849af 100644
--- 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
+++ 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
@@ -203,6 +203,8 @@ public class TestCompatibilityUtils {
       {OperationType.DROP_SEMANTIC_MODEL, Operation.DROP_SEMANTIC_MODEL},
       {OperationType.LOAD_SEMANTIC_MODEL, Operation.LOAD_SEMANTIC_MODEL},
       {OperationType.LIST_SEMANTIC_MODEL, Operation.LIST_SEMANTIC_MODEL},
+      {OperationType.IMPORT_SEMANTIC_MODEL, Operation.IMPORT_SEMANTIC_MODEL},
+      {OperationType.EXPORT_SEMANTIC_MODEL, Operation.EXPORT_SEMANTIC_MODEL},
       {OperationType.REGISTER_FUNCTION, Operation.REGISTER_FUNCTION},
       {OperationType.GET_FUNCTION, Operation.GET_FUNCTION},
       {OperationType.ALTER_FUNCTION, Operation.ALTER_FUNCTION},
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelDispatcher.java
new file mode 100644
index 0000000000..96e001c425
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelDispatcher.java
@@ -0,0 +1,97 @@
+/*
+ * 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.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieFormat;
+import org.apache.gravitino.semantic.SemanticModel;
+import org.apache.gravitino.semantic.SemanticModelCatalog;
+import org.apache.gravitino.semantic.SemanticModelDefinition;
+import org.junit.jupiter.api.Test;
+
+/** Tests the Core Semantic Model dispatcher contract. */
+public class TestSemanticModelDispatcher {
+
+  @Test
+  void testDispatcherDoesNotExposeTwoArgumentImport() {
+    
assertFalse(SemanticModelCatalog.class.isAssignableFrom(SemanticModelDispatcher.class));
+    assertThrows(
+        NoSuchMethodException.class,
+        () ->
+            SemanticModelDispatcher.class.getMethod(
+                "importOssieDocument", Namespace.class, OssieDocument.class));
+  }
+
+  @Test
+  void testParsedImportRequiresAnExplicitImplementation() throws 
NoSuchMethodException {
+    assertFalse(
+        SemanticModelDispatcher.class
+            .getMethod(
+                "importSemanticModel",
+                NameIdentifier.class,
+                String.class,
+                SemanticModelDefinition.class,
+                Map.class)
+            .isDefault());
+  }
+
+  @Test
+  void testExportRequiresAnExplicitImplementation() throws 
NoSuchMethodException {
+    assertFalse(
+        SemanticModelDispatcher.class
+            .getMethod("exportOssieDocument", NameIdentifier.class, 
OssieFormat.class)
+            .isDefault());
+  }
+
+  @Test
+  void testSemanticModelExists() {
+    SemanticModelDispatcher dispatcher = mock(SemanticModelDispatcher.class, 
CALLS_REAL_METHODS);
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "schema", 
"model");
+    
when(dispatcher.loadSemanticModel(ident)).thenReturn(mock(SemanticModel.class));
+    assertTrue(dispatcher.semanticModelExists(ident));
+
+    when(dispatcher.loadSemanticModel(ident))
+        .thenThrow(new NoSuchSemanticModelException("Semantic Model does not 
exist: %s", ident));
+    assertFalse(dispatcher.semanticModelExists(ident));
+  }
+
+  @Test
+  void testSemanticModelExistsPropagatesOtherFailures() {
+    SemanticModelDispatcher dispatcher = mock(SemanticModelDispatcher.class, 
CALLS_REAL_METHODS);
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "schema", 
"model");
+    IllegalStateException failure = new IllegalStateException("Unable to load 
Semantic Model");
+    when(dispatcher.loadSemanticModel(ident)).thenThrow(failure);
+
+    assertSame(
+        failure,
+        assertThrows(IllegalStateException.class, () -> 
dispatcher.semanticModelExists(ident)));
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelNormalizeDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelNormalizeDispatcher.java
index 154eec5b69..0f62ca6e09 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelNormalizeDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelNormalizeDispatcher.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.catalog;
 import static org.apache.gravitino.semantic.OssieVersion.DEFAULT_VERSION;
 import static 
org.apache.gravitino.semantic.SemanticModel.PROPERTY_OSSIE_VERSION;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -41,7 +42,6 @@ import org.apache.gravitino.connector.capability.Capability;
 import org.apache.gravitino.connector.capability.CapabilityResult;
 import org.apache.gravitino.semantic.Dataset;
 import org.apache.gravitino.semantic.OssieDocument;
-import org.apache.gravitino.semantic.OssieDocumentConverter;
 import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
@@ -124,20 +124,22 @@ public class TestSemanticModelNormalizeDispatcher {
   }
 
   @Test
-  public void testOssieImportUsesNativeCreateNormalization() {
+  public void testOssieImportNormalizesAndForwardsNativeValues() {
     SemanticModel model = mock(SemanticModel.class);
     Map<String, String> properties = Map.of(PROPERTY_OSSIE_VERSION, 
DEFAULT_VERSION);
-    when(delegate.createSemanticModel(
+    when(delegate.importSemanticModel(
             eq(NORMALIZED_IDENT), isNull(), 
any(SemanticModelDefinition.class), eq(properties)))
         .thenReturn(model);
 
     assertEquals(
-        model, dispatcher.importOssieDocument(INPUT_NAMESPACE, 
ossieDocument("SalesModel")));
+        model,
+        dispatcher.importOssieDocument(
+            INPUT_NAMESPACE, ossieDocument("SalesModel"), definitionToValidate 
-> {}));
 
     ArgumentCaptor<SemanticModelDefinition> definition =
         ArgumentCaptor.forClass(SemanticModelDefinition.class);
     verify(delegate)
-        .createSemanticModel(eq(NORMALIZED_IDENT), isNull(), 
definition.capture(), eq(properties));
+        .importSemanticModel(eq(NORMALIZED_IDENT), isNull(), 
definition.capture(), eq(properties));
     assertEquals(definition(), definition.getValue());
     verifyNoMoreInteractions(delegate);
   }
@@ -146,23 +148,20 @@ public class TestSemanticModelNormalizeDispatcher {
   public void testOssieImportRejectsInvalidModelName() {
     assertThrows(
         IllegalArgumentException.class,
-        () -> dispatcher.importOssieDocument(INPUT_NAMESPACE, 
ossieDocument("invalid model")));
+        () ->
+            dispatcher.importOssieDocument(
+                INPUT_NAMESPACE, ossieDocument("invalid model"), definition -> 
{}));
     verifyNoInteractions(delegate);
   }
 
   @Test
-  public void testOssieExportUsesNativeLoadNormalization() {
-    SemanticModel model = mock(SemanticModel.class);
-    when(model.name()).thenReturn("SalesModel");
-    when(model.definition()).thenReturn(definition());
-    when(model.properties()).thenReturn(Map.of());
-    when(delegate.loadSemanticModel(NORMALIZED_IDENT)).thenReturn(model);
+  public void testOssieExportNormalizesAndForwardsWithoutLoading() {
+    OssieDocument document = OssieDocument.json("{}");
+    when(delegate.exportOssieDocument(NORMALIZED_IDENT, 
OssieFormat.JSON)).thenReturn(document);
 
-    OssieDocument document = dispatcher.exportOssieDocument(INPUT_IDENT, 
OssieFormat.JSON);
+    assertSame(document, dispatcher.exportOssieDocument(INPUT_IDENT, 
OssieFormat.JSON));
 
-    assertEquals(OssieFormat.JSON, document.format());
-    assertEquals("SalesModel", 
OssieDocumentConverter.importDocument(document).name());
-    verify(delegate).loadSemanticModel(NORMALIZED_IDENT);
+    verify(delegate).exportOssieDocument(NORMALIZED_IDENT, OssieFormat.JSON);
     verifyNoMoreInteractions(delegate);
   }
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
index 98da2ea078..59c0eb7f1a 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
@@ -37,6 +37,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
@@ -44,6 +45,7 @@ import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.Schema;
+import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
@@ -347,11 +349,12 @@ public class TestSemanticModelOperationDispatcher {
     assertEquals(format, document.format());
     assertThrows(
         SemanticModelAlreadyExistsException.class,
-        () -> dispatcher.importOssieDocument(NAMESPACE, document));
+        () -> dispatcher.importOssieDocument(NAMESPACE, document, 
definitionToValidate -> {}));
     assertSame(original, dispatcher.loadSemanticModel(MODEL_IDENT));
 
     assertTrue(dispatcher.dropSemanticModel(MODEL_IDENT));
-    SemanticModel imported = dispatcher.importOssieDocument(NAMESPACE, 
document);
+    SemanticModel imported =
+        dispatcher.importOssieDocument(NAMESPACE, document, 
definitionToValidate -> {});
     assertEquals(original.name(), imported.name());
     assertEquals(original.comment(), imported.comment());
     assertEquals(original.definition(), imported.definition());
@@ -359,6 +362,23 @@ public class TestSemanticModelOperationDispatcher {
     assertSame(imported, dispatcher.loadSemanticModel(MODEL_IDENT));
   }
 
+  @Test
+  public void testParsedImportCreatesModelFromNativeValues() {
+    SemanticModelDefinition definition = validDefinition();
+    SemanticModel imported =
+        dispatcher.importSemanticModel(MODEL_IDENT, "Sales", definition, 
Map.of("domain", "sales"));
+
+    assertEquals(MODEL_IDENT.name(), imported.name());
+    assertEquals("Sales", imported.comment());
+    assertEquals(definition, imported.definition());
+    assertEquals("sales", imported.properties().get("domain"));
+    assertEquals(DEFAULT_VERSION, 
imported.properties().get(PROPERTY_OSSIE_VERSION));
+    assertSame(imported, dispatcher.loadSemanticModel(MODEL_IDENT));
+    assertThrows(
+        SemanticModelAlreadyExistsException.class,
+        () -> dispatcher.importSemanticModel(MODEL_IDENT, null, definition, 
Map.of()));
+  }
+
   @Test
   public void testOssieImportReusesDefinitionValidationWithoutPersisting() {
     OssieDocument invalid =
@@ -379,17 +399,51 @@ public class TestSemanticModelOperationDispatcher {
 
     assertThrows(
         IllegalSemanticModelException.class,
-        () -> dispatcher.importOssieDocument(NAMESPACE, invalid));
+        () -> dispatcher.importOssieDocument(NAMESPACE, invalid, definition -> 
{}));
     assertFalse(dispatcher.semanticModelExists(MODEL_IDENT));
   }
 
+  @Test
+  public void testImportSourceValidationRunsBeforePersistence() {
+    AtomicInteger validations = new AtomicInteger();
+    SemanticModel imported =
+        dispatcher.importOssieDocument(
+            NAMESPACE,
+            ossieDocument(),
+            definition -> {
+              assertEquals("orders", definition.datasets()[0].name());
+              assertFalse(dispatcher.semanticModelExists(MODEL_IDENT));
+              validations.incrementAndGet();
+            });
+    assertEquals(1, validations.get());
+    assertSame(imported, dispatcher.loadSemanticModel(MODEL_IDENT));
+  }
+
+  @Test
+  public void testImportSourceValidationFailureDoesNotPersist() {
+    ForbiddenException denied = new ForbiddenException("source denied");
+    assertSame(
+        denied,
+        assertThrows(
+            ForbiddenException.class,
+            () ->
+                dispatcher.importOssieDocument(
+                    NAMESPACE,
+                    ossieDocument(),
+                    definition -> {
+                      throw denied;
+                    })));
+    assertFalse(dispatcher.semanticModelExists(MODEL_IDENT));
+    verify(schemaDispatcher, never()).loadSchema(SCHEMA_IDENT);
+  }
+
   @Test
   public void testOssieImportRejectsWrongFormatBeforeCatalogLookup() {
     assertThrows(
         IllegalSemanticModelException.class,
         () ->
             dispatcher.importOssieDocument(
-                NAMESPACE, OssieDocument.json(ossieDocument().content())));
+                NAMESPACE, OssieDocument.json(ossieDocument().content()), 
definition -> {}));
     verify(catalogManager, never()).loadCatalog(METADATA_CATALOG_IDENT);
   }
 
@@ -402,7 +456,7 @@ public class TestSemanticModelOperationDispatcher {
         .thenThrow(new NoSuchSchemaException("Schema does not exist"));
     assertThrows(
         NoSuchSchemaException.class,
-        () -> dispatcher.importOssieDocument(NAMESPACE, ossieDocument()));
+        () -> dispatcher.importOssieDocument(NAMESPACE, ossieDocument(), 
definition -> {}));
   }
 
   private static OssieDocument ossieDocument() {
diff --git 
a/core/src/test/java/org/apache/gravitino/hook/TestSemanticModelHookDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/hook/TestSemanticModelHookDispatcher.java
index 48574ef2b6..737bd1343a 100644
--- 
a/core/src/test/java/org/apache/gravitino/hook/TestSemanticModelHookDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/hook/TestSemanticModelHookDispatcher.java
@@ -48,6 +48,7 @@ import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
@@ -96,6 +97,35 @@ public class TestSemanticModelHookDispatcher {
     }
   }
 
+  @Test
+  public void testImportUsesItsOwnHookAndSetsCreatorAsOwner() {
+    SemanticModelDispatcher dispatcher = 
Mockito.mock(SemanticModelDispatcher.class);
+    SemanticModelDefinition definition = 
Mockito.mock(SemanticModelDefinition.class);
+    SemanticModel imported = Mockito.mock(SemanticModel.class);
+    ImmutableMap<String, String> properties = ImmutableMap.of("domain", 
"sales");
+    Mockito.when(dispatcher.importSemanticModel(IDENT, "comment", definition, 
properties))
+        .thenReturn(imported);
+    OwnerDispatcher ownerDispatcher = Mockito.mock(OwnerDispatcher.class);
+    SemanticModelHookDispatcher hook =
+        new SemanticModelHookDispatcher(dispatcher, () -> ownerDispatcher);
+
+    assertSame(imported, hook.importSemanticModel(IDENT, "comment", 
definition, properties));
+
+    InOrder order = Mockito.inOrder(dispatcher, ownerDispatcher);
+    order.verify(dispatcher).importSemanticModel(IDENT, "comment", definition, 
properties);
+    ArgumentCaptor<MetadataObject> object = 
ArgumentCaptor.forClass(MetadataObject.class);
+    order
+        .verify(ownerDispatcher)
+        .setOwner(
+            eq("metalake1"),
+            object.capture(),
+            eq(AuthConstants.ANONYMOUS_USER),
+            eq(Owner.Type.USER));
+    assertEquals("catalog1.schema1.sales_model", object.getValue().fullName());
+    assertEquals(MetadataObject.Type.SEMANTIC_MODEL, object.getValue().type());
+    Mockito.verifyNoMoreInteractions(dispatcher, ownerDispatcher);
+  }
+
   @Test
   public void testCreateSemanticModelSetsOwnerWithNormalizedIdentifier() 
throws Exception {
     // The NormalizeDispatcher case-folds the parent schema against the 
catalog capability while
diff --git 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestSemanticModelImportExportEvent.java
 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestSemanticModelImportExportEvent.java
new file mode 100644
index 0000000000..25da8956a5
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestSemanticModelImportExportEvent.java
@@ -0,0 +1,415 @@
+/*
+ * 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.listener.api.event;
+
+import static 
org.apache.gravitino.semantic.SemanticModel.PROPERTY_OSSIE_VERSION;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.audit.AuditLog;
+import org.apache.gravitino.audit.v2.SimpleAuditLogV2;
+import org.apache.gravitino.authorization.Owner;
+import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.CatalogTestUtils;
+import org.apache.gravitino.catalog.SemanticModelDispatcher;
+import org.apache.gravitino.catalog.SemanticModelNormalizeDispatcher;
+import org.apache.gravitino.catalog.SemanticModelOperationDispatcher;
+import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.IllegalSemanticModelException;
+import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
+import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
+import org.apache.gravitino.hook.SemanticModelHookDispatcher;
+import org.apache.gravitino.listener.DummyEventListener;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.SemanticModelEventDispatcher;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelPreEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelPreEvent;
+import org.apache.gravitino.semantic.Dataset;
+import org.apache.gravitino.semantic.OssieDocument;
+import org.apache.gravitino.semantic.OssieDocumentConverter;
+import org.apache.gravitino.semantic.OssieFormat;
+import org.apache.gravitino.semantic.SemanticModel;
+import org.apache.gravitino.semantic.SemanticModelDefinition;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+
+class TestSemanticModelImportExportEvent {
+  private static final Namespace NAMESPACE = Namespace.of("metalake", 
"catalog", "MixedSchema");
+  private static final NameIdentifier IDENT = NameIdentifier.of(NAMESPACE, 
"SalesModel");
+  private static final NameIdentifier NORMALIZED_IDENT =
+      NameIdentifier.of("metalake", "catalog", "mixedschema", "SalesModel");
+
+  private SemanticModelDispatcher operation;
+  private SemanticModelNormalizeDispatcher normalizer;
+  private SemanticModelHookDispatcher hook;
+  private SemanticModelEventDispatcher dispatcher;
+  private OwnerDispatcher owners;
+  private DummyEventListener listener;
+  private SemanticModel model;
+  private boolean authorizationEnabled;
+
+  @BeforeEach
+  void setUp() {
+    authorizationEnabled = true;
+    owners = mock(OwnerDispatcher.class);
+    operation = mock(SemanticModelOperationDispatcher.class);
+    doCallRealMethod().when(operation).importSemanticModel(any(), any(), 
any(), any());
+    doCallRealMethod().when(operation).exportOssieDocument(any(), any());
+    model = mock(SemanticModel.class);
+    when(model.name()).thenReturn(IDENT.name());
+    when(model.definition()).thenReturn(definition());
+    when(model.properties()).thenReturn(Map.of());
+    when(operation.createSemanticModel(any(), any(), any(), 
any())).thenReturn(model);
+    when(operation.loadSemanticModel(NORMALIZED_IDENT)).thenReturn(model);
+    CatalogManager catalogs = mock(CatalogManager.class);
+    BaseCatalog<?> catalog = mock(BaseCatalog.class);
+    when(catalog.capability()).thenReturn(new ParentNormalizingCapability());
+    CatalogTestUtils.mockDoWithCatalog(catalogs, catalog);
+    hook =
+        spy(new SemanticModelHookDispatcher(operation, () -> 
authorizationEnabled ? owners : null));
+    normalizer = spy(new SemanticModelNormalizeDispatcher(hook, catalogs));
+    listener = new DummyEventListener();
+    dispatcher = new SemanticModelEventDispatcher(new 
EventBus(List.of(listener)), normalizer);
+  }
+
+  @ParameterizedTest
+  @EnumSource(OssieFormat.class)
+  void testImportParsesOnceAndSetsOwnerWithoutCreateEventsOrHooks(OssieFormat 
format) {
+    OssieDocument document = OssieDocumentConverter.exportDocument(model, 
format);
+    AtomicInteger validations = new AtomicInteger();
+    try (MockedStatic<OssieDocumentConverter> converter =
+        mockStatic(OssieDocumentConverter.class, CALLS_REAL_METHODS)) {
+      assertSame(
+          model,
+          dispatcher.importOssieDocument(
+              NAMESPACE,
+              document,
+              definition -> {
+                assertEquals(definition(), definition);
+                verify(operation, never()).createSemanticModel(any(), any(), 
any(), any());
+                verifyNoInteractions(owners);
+                validations.incrementAndGet();
+              }));
+      converter.verify(() -> OssieDocumentConverter.importDocument(document));
+    }
+    assertEquals(1, validations.get());
+    verify(operation).importSemanticModel(eq(NORMALIZED_IDENT), any(), 
eq(definition()), any());
+    verify(operation).createSemanticModel(eq(NORMALIZED_IDENT), any(), 
eq(definition()), any());
+    ArgumentCaptor<MetadataObject> object = 
ArgumentCaptor.forClass(MetadataObject.class);
+    verify(owners).setOwner(eq("metalake"), object.capture(), any(), 
eq(Owner.Type.USER));
+    assertEquals("catalog.mixedschema.SalesModel", 
object.getValue().fullName());
+    assertEquals(MetadataObject.Type.SEMANTIC_MODEL, object.getValue().type());
+    verifyNoMoreInteractions(owners);
+
+    ImportSemanticModelEvent event =
+        assertEvents(ImportSemanticModelPreEvent.class, 
ImportSemanticModelEvent.class);
+    ImportSemanticModelPreEvent pre =
+        (ImportSemanticModelPreEvent) listener.getPreEvents().getFirst();
+    assertEquals(NAMESPACE, pre.namespace());
+    assertEquals(NameIdentifier.of(NAMESPACE.levels()), pre.identifier());
+    assertEquals(format, pre.format());
+    assertEquals(IDENT, event.identifier());
+    assertEquals(format, event.format());
+    assertEquals(model.definition(), 
event.importedSemanticModelInfo().definition());
+    assertAudit(event, AuditLog.Operation.IMPORT_SEMANTIC_MODEL, 
OperationStatus.SUCCESS);
+  }
+
+  @Test
+  void testDirectParsedImportIsRejectedWithoutEventsOrSideEffects() {
+    UnsupportedOperationException failure =
+        assertThrows(
+            UnsupportedOperationException.class,
+            () -> dispatcher.importSemanticModel(IDENT, null, definition(), 
Map.of()));
+
+    assertTrue(failure.getMessage().contains("importOssieDocument"));
+    verifyNoInteractions(normalizer, hook, operation, owners);
+    assertEquals(0, listener.getPreEvents().size());
+    assertEquals(0, listener.getPostEvents().size());
+  }
+
+  @Test
+  void testImportStillValidatesSourcesWhenOwnershipIsDisabled() {
+    authorizationEnabled = false;
+    AtomicInteger validations = new AtomicInteger();
+    dispatcher.importOssieDocument(
+        NAMESPACE, document(), definition -> validations.incrementAndGet());
+    assertEquals(1, validations.get());
+    assertEvents(ImportSemanticModelPreEvent.class, 
ImportSemanticModelEvent.class);
+    verifyNoInteractions(owners);
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"parse", "source", "normalize", "create", "owner"})
+  void testImportFailuresEmitOnlyImportFailure(String stage) {
+    OssieDocument document = document();
+    RuntimeException expected = new ForbiddenException("source denied");
+    if (stage.equals("parse")) {
+      document = OssieDocument.json("{invalid json");
+    } else if (stage.equals("normalize")) {
+      document = OssieDocument.yaml(document.content().replace("SalesModel", 
"invalid model"));
+    } else if (stage.equals("create")) {
+      expected = new SemanticModelAlreadyExistsException("already exists");
+      doThrow(expected).when(operation).createSemanticModel(any(), any(), 
any(), any());
+    } else if (stage.equals("owner")) {
+      expected = new IllegalStateException("owner assignment failed");
+      doThrow(expected).when(owners).setOwner(any(), any(), any(), any());
+    }
+    OssieDocument request = document;
+    RuntimeException failure = expected;
+    RuntimeException thrown =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                dispatcher.importOssieDocument(
+                    NAMESPACE,
+                    request,
+                    definition -> {
+                      if (stage.equals("source")) throw failure;
+                    }));
+    ImportSemanticModelFailureEvent event =
+        assertEvents(ImportSemanticModelPreEvent.class, 
ImportSemanticModelFailureEvent.class);
+    assertSame(thrown, event.exception());
+    assertEquals(NAMESPACE, event.namespace());
+    assertEquals(NameIdentifier.of(NAMESPACE.levels()), event.identifier());
+    assertEquals(document.format(), event.format());
+    assertAudit(event, AuditLog.Operation.IMPORT_SEMANTIC_MODEL, 
OperationStatus.FAILURE);
+    if (!stage.equals("owner")) verifyNoInteractions(owners);
+    if (List.of("parse", "source", "normalize").contains(stage)) {
+      verify(operation, never()).createSemanticModel(any(), any(), any(), 
any());
+    } else {
+      assertSame(expected, thrown);
+    }
+  }
+
+  @Test
+  void testImportRejectsMissingValidatorBeforeWriting() {
+    assertThrows(
+        NullPointerException.class,
+        () -> dispatcher.importOssieDocument(NAMESPACE, document(), null));
+    assertEvents(ImportSemanticModelPreEvent.class, 
ImportSemanticModelFailureEvent.class);
+    verifyNoInteractions(operation, owners);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testPreEventRejectionEmitsFailureWithoutInvokingOperation(boolean 
importing) {
+    DummyEventListener rejecting = spy(listener);
+    ForbiddenException denied = new ForbiddenException("listener denied");
+    doThrow(denied).when(rejecting).onPreEvent(any());
+    SemanticModelDispatcher rejectedDispatcher =
+        new SemanticModelEventDispatcher(new EventBus(List.of(rejecting)), 
normalizer);
+    Runnable action =
+        importing
+            ? () -> rejectedDispatcher.importOssieDocument(NAMESPACE, 
document(), definition -> {})
+            : () -> rejectedDispatcher.exportOssieDocument(IDENT, 
OssieFormat.YAML);
+    assertSame(denied, assertThrows(ForbiddenException.class, action::run));
+    verifyNoInteractions(normalizer, operation, owners);
+    assertEquals(1, rejecting.getPostEvents().size());
+    FailureEvent failure =
+        assertInstanceOf(FailureEvent.class, 
rejecting.getPostEvents().getFirst());
+    assertEquals(
+        importing ? OperationType.IMPORT_SEMANTIC_MODEL : 
OperationType.EXPORT_SEMANTIC_MODEL,
+        failure.operationType());
+    assertSame(denied, failure.exception());
+  }
+
+  @Test
+  void testNullImportDocumentStillEmitsFailure() {
+    assertThrows(
+        NullPointerException.class,
+        () -> dispatcher.importOssieDocument(NAMESPACE, null, definition -> 
{}));
+    ImportSemanticModelFailureEvent event =
+        assertEvents(ImportSemanticModelPreEvent.class, 
ImportSemanticModelFailureEvent.class);
+    assertNull(event.format());
+    verifyNoInteractions(operation, owners);
+  }
+
+  @ParameterizedTest
+  @EnumSource(OssieFormat.class)
+  void testExportDoesNotEmitLoadEventsOrRunLoadHooks(OssieFormat format) {
+    OssieDocument document = dispatcher.exportOssieDocument(IDENT, format);
+    assertEquals(format, document.format());
+    assertEquals(IDENT.name(), 
OssieDocumentConverter.importDocument(document).name());
+    verify(operation).loadSemanticModel(NORMALIZED_IDENT);
+    verifyNoInteractions(owners);
+    ExportSemanticModelEvent event =
+        assertEvents(ExportSemanticModelPreEvent.class, 
ExportSemanticModelEvent.class);
+    ExportSemanticModelPreEvent pre =
+        (ExportSemanticModelPreEvent) listener.getPreEvents().getFirst();
+    assertEquals(IDENT, pre.identifier());
+    assertEquals(format, pre.format());
+    assertEquals(IDENT, event.identifier());
+    assertEquals(format, event.format());
+    assertAudit(event, AuditLog.Operation.EXPORT_SEMANTIC_MODEL, 
OperationStatus.SUCCESS);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testExportLoadAndConversionFailures(boolean conversionFailure) {
+    if (conversionFailure) {
+      when(model.properties()).thenReturn(Map.of(PROPERTY_OSSIE_VERSION, " "));
+    } else {
+      when(operation.loadSemanticModel(NORMALIZED_IDENT))
+          .thenThrow(new NoSuchSemanticModelException("missing model"));
+    }
+    RuntimeException thrown =
+        assertThrows(
+            RuntimeException.class, () -> 
dispatcher.exportOssieDocument(IDENT, OssieFormat.YAML));
+    assertEquals(
+        conversionFailure
+            ? IllegalSemanticModelException.class
+            : NoSuchSemanticModelException.class,
+        thrown.getClass());
+    ExportSemanticModelFailureEvent event =
+        assertEvents(ExportSemanticModelPreEvent.class, 
ExportSemanticModelFailureEvent.class);
+    assertSame(thrown, event.exception());
+    assertEquals(IDENT, event.identifier());
+    assertEquals(OssieFormat.YAML, event.format());
+    assertAudit(event, AuditLog.Operation.EXPORT_SEMANTIC_MODEL, 
OperationStatus.FAILURE);
+    verifyNoInteractions(owners);
+  }
+
+  @Test
+  void testNullExportFormatStillEmitsFailure() {
+    assertThrows(NullPointerException.class, () -> 
dispatcher.exportOssieDocument(IDENT, null));
+    ExportSemanticModelFailureEvent event =
+        assertEvents(ExportSemanticModelPreEvent.class, 
ExportSemanticModelFailureEvent.class);
+    assertNull(event.format());
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testImportAndExportRetainInitiatingUser(boolean fail) {
+    authorizationEnabled = false;
+    if (fail) {
+      doThrow(new IllegalArgumentException("rejected"))
+          .when(operation)
+          .createSemanticModel(any(), any(), any(), any());
+      doThrow(new 
IllegalArgumentException("rejected")).when(operation).loadSemanticModel(any());
+    }
+    for (boolean importing : new boolean[] {true, false}) {
+      listener.clear();
+      try (MockedStatic<PrincipalUtils> principal = 
mockStatic(PrincipalUtils.class)) {
+        
principal.when(PrincipalUtils::getCurrentUserName).thenReturn("initiator", 
"changed");
+        Runnable action =
+            importing
+                ? () -> dispatcher.importOssieDocument(NAMESPACE, document(), 
definition -> {})
+                : () -> dispatcher.exportOssieDocument(IDENT, 
OssieFormat.JSON);
+        if (fail) assertThrows(IllegalArgumentException.class, action::run);
+        else action.run();
+        assertEquals("initiator", listener.getPreEvents().getFirst().user());
+        assertEquals("initiator", listener.getPostEvents().getFirst().user());
+        principal.verify(PrincipalUtils::getCurrentUserName);
+      }
+    }
+  }
+
+  private <T extends Event> T assertEvents(Class<? extends PreEvent> pre, 
Class<T> post) {
+    assertEquals(1, listener.getPreEvents().size());
+    assertEquals(1, listener.getPostEvents().size());
+    PreEvent preEvent = assertInstanceOf(pre, 
listener.getPreEvents().getFirst());
+    T postEvent = assertInstanceOf(post, listener.getPostEvents().getFirst());
+    assertEquals(preEvent.operationType(), postEvent.operationType());
+    assertEquals(OperationStatus.UNPROCESSED, preEvent.operationStatus());
+    verify(normalizer, never()).createSemanticModel(any(), any(), any(), 
any());
+    verify(normalizer, never()).loadSemanticModel(any());
+    verify(hook, never()).createSemanticModel(any(), any(), any(), any());
+    verify(hook, never()).loadSemanticModel(any());
+    return postEvent;
+  }
+
+  @SuppressWarnings("deprecation")
+  private void assertAudit(Event event, AuditLog.Operation operation, 
OperationStatus status) {
+    assertEquals(operation, AuditLog.Operation.fromEvent(event));
+    SimpleAuditLogV2 audit = new SimpleAuditLogV2(event);
+    assertEquals(operation, audit.operation());
+    assertEquals(OperationType.valueOf(operation.name()), 
audit.operationType());
+    assertEquals(status, audit.operationStatus());
+    assertEquals(event.identifier().toString(), audit.identifier());
+    assertEquals(event.user(), audit.user());
+  }
+
+  private static OssieDocument document() {
+    return OssieDocument.yaml(
+        "version: 0.2.0.dev0\nname: SalesModel\ndatasets:\n"
+            + "  - name: orders\n    source: sales.mart.orders\n");
+  }
+
+  private static SemanticModelDefinition definition() {
+    return SemanticModelDefinition.builder()
+        .withDatasets(
+            new Dataset[] {
+              Dataset.builder()
+                  .withName("orders")
+                  .withSource(NameIdentifier.of("sales", "mart", "orders"))
+                  .build()
+            })
+        .build();
+  }
+
+  private static class ParentNormalizingCapability implements Capability {
+    @Override
+    public CapabilityResult caseSensitiveOnName(Scope scope) {
+      return CapabilityResult.unsupported("Case-insensitive schema");
+    }
+
+    @Override
+    public String normalizeName(Scope scope, String name) {
+      return scope == Scope.SCHEMA ? name.toLowerCase(Locale.ROOT) : name;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/semantic/TestOssieDocumentConverter.java
 
b/core/src/test/java/org/apache/gravitino/semantic/TestOssieDocumentConverter.java
index 366151e845..37f83e8a85 100644
--- 
a/core/src/test/java/org/apache/gravitino/semantic/TestOssieDocumentConverter.java
+++ 
b/core/src/test/java/org/apache/gravitino/semantic/TestOssieDocumentConverter.java
@@ -41,7 +41,7 @@ import org.apache.gravitino.Namespace;
 import org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.SemanticModelEntity;
-import 
org.apache.gravitino.semantic.OssieDocumentConverter.ImportedSemanticModel;
+import 
org.apache.gravitino.semantic.OssieDocumentConverter.ParsedSemanticModel;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
@@ -89,8 +89,7 @@ public class TestOssieDocumentConverter {
             data: '{"domain":"sales"}'
         """;
 
-    ImportedSemanticModel yamlModel =
-        OssieDocumentConverter.importDocument(OssieDocument.yaml(yaml));
+    ParsedSemanticModel yamlModel = 
OssieDocumentConverter.importDocument(OssieDocument.yaml(yaml));
     assertEquals("sales", yamlModel.name());
     assertEquals("Governed sales definitions", yamlModel.comment());
     assertEquals(
@@ -118,8 +117,7 @@ public class TestOssieDocumentConverter {
           ]
         }
         """;
-    ImportedSemanticModel jsonModel =
-        OssieDocumentConverter.importDocument(OssieDocument.json(json));
+    ParsedSemanticModel jsonModel = 
OssieDocumentConverter.importDocument(OssieDocument.json(json));
     assertEquals("inventory", jsonModel.name());
     assertEquals("future-version", 
jsonModel.properties().get(PROPERTY_OSSIE_VERSION));
     assertEquals(
@@ -158,7 +156,7 @@ public class TestOssieDocumentConverter {
     assertEquals(JSON_MAPPER.readTree("{\"domain\":\"sales\"}"), properties);
     assertFalse(properties.has(PROPERTY_OSSIE_VERSION));
 
-    ImportedSemanticModel jsonRoundTrip = 
OssieDocumentConverter.importDocument(jsonDocument);
+    ParsedSemanticModel jsonRoundTrip = 
OssieDocumentConverter.importDocument(jsonDocument);
     assertEquals(semanticModel.name(), jsonRoundTrip.name());
     assertEquals(semanticModel.comment(), jsonRoundTrip.comment());
     assertEquals(semanticModel.definition(), jsonRoundTrip.definition());
@@ -169,7 +167,7 @@ public class TestOssieDocumentConverter {
     assertEquals(OssieFormat.YAML, yamlDocument.format());
     assertTrue(yamlDocument.content().contains("version:"));
     assertFalse(yamlDocument.content().contains("semantic_model:"));
-    ImportedSemanticModel yamlRoundTrip = 
OssieDocumentConverter.importDocument(yamlDocument);
+    ParsedSemanticModel yamlRoundTrip = 
OssieDocumentConverter.importDocument(yamlDocument);
     assertEquals(semanticModel.definition(), yamlRoundTrip.definition());
     assertEquals(semanticModel.properties(), yamlRoundTrip.properties());
   }
@@ -187,7 +185,7 @@ public class TestOssieDocumentConverter {
     JsonNode json = JSON_MAPPER.readTree(document.content());
     assertEquals("`sales.eu`.`ma``rt`.`ord.ers`", 
json.at("/datasets/0/source").textValue());
 
-    ImportedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
+    ParsedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
     assertEquals(source, roundTrip.definition().datasets()[0].source());
   }
 
@@ -199,7 +197,7 @@ public class TestOssieDocumentConverter {
     JsonNode json = JSON_MAPPER.readTree(document.content());
 
     assertEquals(DEFAULT_VERSION, json.path("version").textValue());
-    ImportedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
+    ParsedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
     assertEquals(DEFAULT_VERSION, 
roundTrip.properties().get(PROPERTY_OSSIE_VERSION));
   }
 
@@ -422,7 +420,7 @@ public class TestOssieDocumentConverter {
   @ParameterizedTest
   @EnumSource(OssieFormat.class)
   public void testConsumesEmptyPropertiesExtension(OssieFormat format) throws 
Exception {
-    ImportedSemanticModel request =
+    ParsedSemanticModel request =
         OssieDocumentConverter.importDocument(
             ossieDocument(documentWithExtension(GRAVITINO_PROPERTIES_VENDOR, 
"{}"), format));
     assertEquals(Map.of(PROPERTY_OSSIE_VERSION, DEFAULT_VERSION), 
request.properties());
@@ -441,7 +439,7 @@ public class TestOssieDocumentConverter {
       "not JSON"
     };
     for (String payload : payloads) {
-      ImportedSemanticModel request =
+      ParsedSemanticModel request =
           OssieDocumentConverter.importDocument(
               ossieDocument(documentWithExtension("GRAVITINO", payload), 
format));
       assertEquals(Map.of(PROPERTY_OSSIE_VERSION, DEFAULT_VERSION), 
request.properties());
@@ -451,7 +449,7 @@ public class TestOssieDocumentConverter {
           semanticModel(
               request.definition(),
               Map.of("owner", "analytics", PROPERTY_OSSIE_VERSION, 
DEFAULT_VERSION));
-      ImportedSemanticModel roundTrip =
+      ParsedSemanticModel roundTrip =
           OssieDocumentConverter.importDocument(
               OssieDocumentConverter.exportDocument(model, format));
       assertEquals(model.definition(), roundTrip.definition());
@@ -478,7 +476,7 @@ public class TestOssieDocumentConverter {
             SemanticModelDefinition.builder().withDatasets(new Dataset[] 
{dataset}).build(),
             Map.of("owner", "analytics", PROPERTY_OSSIE_VERSION, 
DEFAULT_VERSION));
 
-    ImportedSemanticModel roundTrip =
+    ParsedSemanticModel roundTrip =
         
OssieDocumentConverter.importDocument(OssieDocumentConverter.exportDocument(model,
 format));
     assertEquals(model.definition(), roundTrip.definition());
     assertEquals(model.properties(), roundTrip.properties());
@@ -557,7 +555,7 @@ public class TestOssieDocumentConverter {
             .path("dialect")
             .textValue());
 
-    ImportedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
+    ParsedSemanticModel roundTrip = 
OssieDocumentConverter.importDocument(document);
     assertEquals("TRINO", 
roundTrip.definition().metrics()[0].expression().dialects()[0].dialect());
   }
 
@@ -640,7 +638,7 @@ public class TestOssieDocumentConverter {
         """;
     OssieDocument input =
         format == OssieFormat.JSON ? OssieDocument.json(content) : 
OssieDocument.yaml(content);
-    ImportedSemanticModel imported = 
OssieDocumentConverter.importDocument(input);
+    ParsedSemanticModel imported = 
OssieDocumentConverter.importDocument(input);
     AIContextObject context = imported.definition().aiContext().object();
     assertEquals("Use certified definitions", context.instructions());
     assertEquals(2, context.synonyms().length);
@@ -676,7 +674,7 @@ public class TestOssieDocumentConverter {
     root.putArray("relationships");
     root.putArray("metrics");
 
-    ImportedSemanticModel imported =
+    ParsedSemanticModel imported =
         OssieDocumentConverter.importDocument(ossieDocument(root, format));
     Dataset converted = imported.definition().datasets()[0];
     assertNull(imported.comment());
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
index 5e2227bf63..21cc62a478 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
@@ -53,8 +53,6 @@ import 
org.apache.gravitino.dto.responses.SemanticModelResponse;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.metrics.MetricNames;
 import org.apache.gravitino.semantic.OssieDocument;
-import org.apache.gravitino.semantic.OssieDocumentConverter;
-import 
org.apache.gravitino.semantic.OssieDocumentConverter.ImportedSemanticModel;
 import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
@@ -240,14 +238,11 @@ public class SemanticModelOperations {
                 
MediaType.APPLICATION_JSON_TYPE.isCompatible(headers.getMediaType())
                     ? OssieDocument.json(document)
                     : OssieDocument.yaml(document);
-            ImportedSemanticModel imported = 
OssieDocumentConverter.importDocument(ossieDocument);
-            sourceValidator.validate(metalake, imported.definition());
             SemanticModel semanticModel =
-                dispatcher.createSemanticModel(
-                    NameIdentifierUtil.ofSemanticModel(metalake, catalog, 
schema, imported.name()),
-                    imported.comment(),
-                    imported.definition(),
-                    imported.properties());
+                dispatcher.importOssieDocument(
+                    NamespaceUtil.ofSemanticModel(metalake, catalog, schema),
+                    ossieDocument,
+                    definition -> sourceValidator.validate(metalake, 
definition));
             LOG.info(
                 "Apache Ossie Semantic Model imported: {}.{}.{}.{}",
                 metalake,
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
index 0856bd1879..7b321251ee 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
@@ -34,8 +34,6 @@ import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.json.JsonMapper;
 import java.io.IOException;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
@@ -84,6 +82,15 @@ import org.apache.gravitino.exceptions.NoSuchViewException;
 import org.apache.gravitino.exceptions.OptimisticLockException;
 import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
 import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.SemanticModelEventDispatcher;
+import org.apache.gravitino.listener.api.event.BaseEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ExportSemanticModelPreEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelFailureEvent;
+import 
org.apache.gravitino.listener.api.event.semantic.ImportSemanticModelPreEvent;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.SemanticModelEntity;
 import org.apache.gravitino.rel.Column;
@@ -98,6 +105,7 @@ import org.apache.gravitino.semantic.Dialects;
 import org.apache.gravitino.semantic.Expression;
 import org.apache.gravitino.semantic.Field;
 import org.apache.gravitino.semantic.Metric;
+import org.apache.gravitino.semantic.OssieDocument;
 import org.apache.gravitino.semantic.OssieFormat;
 import org.apache.gravitino.semantic.SemanticModel;
 import org.apache.gravitino.semantic.SemanticModelChange;
@@ -125,7 +133,18 @@ import org.mockito.MockedStatic;
 public class TestSemanticModelOperations extends BaseOperationsTest {
 
   private static final String VND_V1_JSON = 
"application/vnd.gravitino.v1+json";
-  private static final JsonMapper JSON_MAPPER = JsonMapper.builder().build();
+  private static final OssieDocument EXPORTED_YAML =
+      OssieDocument.yaml(
+          """
+          version: 0.2.0.dev0
+          name: sales
+          datasets: []
+          """);
+  private static final OssieDocument EXPORTED_JSON =
+      OssieDocument.json(
+          """
+          {"version":"0.2.0.dev0","name":"sales","datasets":[]}
+          """);
 
   private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
     @Override
@@ -135,6 +154,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   }
 
   private final SemanticModelDispatcher dispatcher = 
mock(SemanticModelDispatcher.class);
+  private final EventBus eventBus = mock(EventBus.class);
   private final TableDispatcher tables = mock(TableDispatcher.class);
   private final ViewDispatcher views = mock(ViewDispatcher.class);
   private boolean tablesSupported = true;
@@ -161,7 +181,9 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         new AbstractBinder() {
           @Override
           protected void configure() {
-            bind(dispatcher).to(SemanticModelDispatcher.class).ranked(2);
+            bind(new SemanticModelEventDispatcher(eventBus, dispatcher))
+                .to(SemanticModelDispatcher.class)
+                .ranked(2);
             bind(new SemanticModelSourceValidator(
                     tables,
                     views,
@@ -181,7 +203,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
 
   @BeforeEach
   void resetDispatcher() {
-    reset(dispatcher, tables, views);
+    reset(dispatcher, tables, views, eventBus);
     tablesSupported = true;
     viewsSupported = true;
     sourceAuthorizer = new PassThroughAuthorizer();
@@ -191,7 +213,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     when(table.columns()).thenReturn(new Column[] {column});
     when(tables.loadTable(any())).thenReturn(table);
     
when(dispatcher.loadSemanticModel(any())).thenReturn(semanticModel("sales", 
null));
-    doCallRealMethod().when(dispatcher).exportOssieDocument(any(), any());
+    doCallRealMethod().when(dispatcher).importOssieDocument(any(), any(), 
any());
   }
 
   @Test
@@ -976,6 +998,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
 
   private void verifyNoModelWrites() {
     verify(dispatcher, never()).createSemanticModel(any(), any(), any(), 
any());
+    verify(dispatcher, never()).importSemanticModel(any(), any(), any(), 
any());
     verify(dispatcher, never()).alterSemanticModel(any(), 
any(SemanticModelChange[].class));
   }
 
@@ -995,13 +1018,13 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   void testImportOssieYamlAndJsonDocuments(String jsonMediaType) {
     NameIdentifier salesIdent = semanticModelIdentifier("sales");
     NameIdentifier inventoryIdent = semanticModelIdentifier("inventory");
-    when(dispatcher.createSemanticModel(
+    when(dispatcher.importSemanticModel(
             eq(salesIdent),
             eq("Sales definitions"),
             any(SemanticModelDefinition.class),
             eq(Map.of("domain", "sales", PROPERTY_OSSIE_VERSION, 
DEFAULT_VERSION))))
         .thenReturn(semanticModel("sales", "Sales definitions"));
-    when(dispatcher.createSemanticModel(
+    when(dispatcher.importSemanticModel(
             eq(inventoryIdent),
             eq(null),
             any(SemanticModelDefinition.class),
@@ -1059,7 +1082,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     ArgumentCaptor<SemanticModelDefinition> definitionCaptor =
         ArgumentCaptor.forClass(SemanticModelDefinition.class);
     verify(dispatcher)
-        .createSemanticModel(
+        .importSemanticModel(
             eq(salesIdent),
             eq("Sales definitions"),
             definitionCaptor.capture(),
@@ -1071,11 +1094,13 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         Dialects.ANSI_SQL,
         
definitionCaptor.getValue().datasets()[0].fields()[0].expression().dialects()[0].dialect());
     verify(dispatcher)
-        .createSemanticModel(
+        .importSemanticModel(
             eq(inventoryIdent),
             eq(null),
             any(SemanticModelDefinition.class),
             eq(Map.of(PROPERTY_OSSIE_VERSION, "future-version")));
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(yaml)), any());
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.json(json)), any());
     verifyNoMoreInteractions(dispatcher);
   }
 
@@ -1083,7 +1108,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   @ValueSource(strings = {"text/yaml", "application/x-yaml"})
   void testImportOssieYamlMediaTypeAliases(String mediaType) {
     NameIdentifier ident = semanticModelIdentifier("marketing");
-    when(dispatcher.createSemanticModel(
+    when(dispatcher.importSemanticModel(
             eq(ident),
             eq(null),
             any(SemanticModelDefinition.class),
@@ -1106,11 +1131,12 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     body.validate();
     Assertions.assertEquals("marketing", body.getSemanticModel().name());
     verify(dispatcher)
-        .createSemanticModel(
+        .importSemanticModel(
             eq(ident),
             eq(null),
             any(SemanticModelDefinition.class),
             eq(Map.of(PROPERTY_OSSIE_VERSION, DEFAULT_VERSION)));
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(yaml)), any());
     verifyNoMoreInteractions(dispatcher);
   }
 
@@ -1118,7 +1144,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   void testImportOssieDocumentWithoutContentTypeDefaultsToYaml()
       throws IOException, InterruptedException {
     NameIdentifier ident = semanticModelIdentifier("marketing");
-    when(dispatcher.createSemanticModel(
+    when(dispatcher.importSemanticModel(
             eq(ident),
             eq(null),
             any(SemanticModelDefinition.class),
@@ -1152,19 +1178,20 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     body.validate();
     Assertions.assertEquals("marketing", body.getSemanticModel().name());
     verify(dispatcher)
-        .createSemanticModel(
+        .importSemanticModel(
             eq(ident),
             eq(null),
             any(SemanticModelDefinition.class),
             eq(Map.of(PROPERTY_OSSIE_VERSION, DEFAULT_VERSION)));
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(yaml)), any());
     verifyNoMoreInteractions(dispatcher);
   }
 
   @Test
-  void testExportOssieYamlAndJsonDocuments() throws Exception {
+  void testExportOssieYamlAndJsonDocuments() {
     NameIdentifier ident = semanticModelIdentifier("sales");
-    when(dispatcher.loadSemanticModel(ident))
-        .thenReturn(semanticModel("sales", "Sales definitions"));
+    when(dispatcher.exportOssieDocument(ident, 
OssieFormat.YAML)).thenReturn(EXPORTED_YAML);
+    when(dispatcher.exportOssieDocument(ident, 
OssieFormat.JSON)).thenReturn(EXPORTED_JSON);
 
     Response yamlResponse = getOssie(semanticModelPath() + "/sales/ossie");
 
@@ -1173,11 +1200,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(
         "attachment; filename=\"sales.ossie.yaml\"",
         yamlResponse.getHeaderString("Content-Disposition"));
-    String yaml = yamlResponse.readEntity(String.class);
-    Assertions.assertTrue(yaml.contains("version:"));
-    Assertions.assertTrue(yaml.contains("name:"));
-    Assertions.assertFalse(yaml.contains("semantic_model:"));
-    Assertions.assertFalse(yaml.contains("definition:"));
+    Assertions.assertEquals(EXPORTED_YAML.content(), 
yamlResponse.readEntity(String.class));
 
     Response jsonResponse =
         target(semanticModelPath() + "/sales/ossie")
@@ -1190,17 +1213,9 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(
         "attachment; filename=\"sales.ossie.json\"",
         jsonResponse.getHeaderString("Content-Disposition"));
-    JsonNode json = 
JSON_MAPPER.readTree(jsonResponse.readEntity(String.class));
-    Assertions.assertEquals("0.2.0.dev0", json.path("version").textValue());
-    Assertions.assertEquals("sales", json.path("name").textValue());
-    Assertions.assertEquals(
-        "semantic_model_catalog.semantic_model_schema.orders",
-        json.at("/datasets/0/source").textValue());
-    Assertions.assertFalse(json.has("semantic_model"));
-    Assertions.assertFalse(json.has("definition"));
+    Assertions.assertEquals(EXPORTED_JSON.content(), 
jsonResponse.readEntity(String.class));
     verify(dispatcher).exportOssieDocument(ident, OssieFormat.YAML);
     verify(dispatcher).exportOssieDocument(ident, OssieFormat.JSON);
-    verify(dispatcher, times(2)).loadSemanticModel(ident);
     verifyNoMoreInteractions(dispatcher);
   }
 
@@ -1227,6 +1242,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
         IllegalArgumentException.class.getSimpleName(),
         "expected yaml or json");
+    verify(dispatcher)
+        .importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(querySource)), any());
     verifyNoMoreInteractions(dispatcher);
   }
 
@@ -1256,6 +1273,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
         IllegalSemanticModelException.class.getSimpleName(),
         "Cannot parse Apache Ossie JSON");
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.json(yaml)), any());
     verifyNoMoreInteractions(dispatcher);
   }
 
@@ -1270,32 +1288,119 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   }
 
   @Test
-  void testOssieImportPropagatesCreateErrors() {
+  void testOssieImportPropagatesDispatcherErrors() {
     String yaml =
         "version: 0.2.0.dev0\nname: sales\ndatasets:\n"
             + "  - name: orders\n    source: sales.mart.orders\n";
     doThrow(new SemanticModelAlreadyExistsException("sales already exists"))
         .when(dispatcher)
-        .createSemanticModel(eq(semanticModelIdentifier("sales")), eq(null), 
any(), any());
+        .importSemanticModel(eq(semanticModelIdentifier("sales")), eq(null), 
any(), any());
     assertError(
         postDocument(semanticModelPath() + "/ossie", yaml, "application/yaml"),
         Response.Status.CONFLICT,
         ErrorConstants.ALREADY_EXISTS_CODE,
         SemanticModelAlreadyExistsException.class.getSimpleName(),
         "sales already exists");
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(yaml)), any());
+    verify(dispatcher)
+        .importSemanticModel(eq(semanticModelIdentifier("sales")), eq(null), 
any(), any());
+    verifyNoMoreInteractions(dispatcher);
   }
 
   @Test
-  void testOssieExportPropagatesLoadErrors() {
+  void testOssieExportPropagatesDispatcherErrors() {
     doThrow(new NoSuchSemanticModelException("sales does not exist"))
         .when(dispatcher)
-        .loadSemanticModel(semanticModelIdentifier("sales"));
+        .exportOssieDocument(semanticModelIdentifier("sales"), 
OssieFormat.YAML);
     assertError(
         getOssie(semanticModelPath() + "/sales/ossie"),
         Response.Status.NOT_FOUND,
         ErrorConstants.NOT_FOUND_CODE,
         NoSuchSemanticModelException.class.getSimpleName(),
         "sales does not exist");
+    verify(dispatcher).exportOssieDocument(semanticModelIdentifier("sales"), 
OssieFormat.YAML);
+    verifyNoMoreInteractions(dispatcher);
+  }
+
+  @ParameterizedTest
+  @CsvSource({
+    "success,200",
+    "missing,400",
+    "denied,403",
+    "connection,502",
+    "unsupported,400",
+    "invalid,400"
+  })
+  void testImportEmitsDedicatedEventsAcrossSourceValidation(String scenario, 
int status) {
+    NameIdentifier source = NameIdentifier.of(metalake, catalog, schema, 
"orders");
+    if (scenario.equals("missing")) {
+      when(tables.loadTable(source)).thenThrow(new 
NoSuchTableException("missing table"));
+      when(views.loadView(source)).thenThrow(new NoSuchViewException("missing 
view"));
+    } else if (scenario.equals("denied")) {
+      sourceAuthorizer = mock(GravitinoAuthorizer.class);
+    } else if (scenario.equals("connection")) {
+      when(tables.loadTable(source)).thenThrow(new 
ConnectionFailedException("connection failed"));
+    } else if (scenario.equals("unsupported")) {
+      tablesSupported = false;
+      viewsSupported = false;
+    }
+    when(dispatcher.importSemanticModel(any(), any(), any(), any()))
+        .thenReturn(semanticModel("sales", null));
+    String document =
+        scenario.equals("invalid")
+            ? "invalid: ["
+            : "version: 0.2.0.dev0\nname: sales\ndatasets:\n  - name: orders\n 
   source: "
+                + catalog
+                + "."
+                + schema
+                + ".orders\n";
+    try (Response response =
+        postDocument(semanticModelPath() + "/ossie", document, 
"application/yaml")) {
+      Assertions.assertEquals(status, response.getStatus());
+    }
+    ArgumentCaptor<BaseEvent> events = 
ArgumentCaptor.forClass(BaseEvent.class);
+    verify(eventBus, times(2)).dispatchEvent(events.capture());
+    Assertions.assertInstanceOf(ImportSemanticModelPreEvent.class, 
events.getAllValues().get(0));
+    if (status == 200) {
+      Assertions.assertInstanceOf(ImportSemanticModelEvent.class, 
events.getAllValues().get(1));
+      verify(dispatcher).importSemanticModel(any(), any(), any(), any());
+    } else {
+      Assertions.assertInstanceOf(
+          ImportSemanticModelFailureEvent.class, events.getAllValues().get(1));
+      verifyNoModelWrites();
+    }
+    if (List.of("denied", "unsupported", "invalid").contains(scenario)) {
+      verifyNoInteractions(tables, views);
+    } else {
+      verify(tables).loadTable(source);
+    }
+    verify(dispatcher).importOssieDocument(eq(namespace), 
eq(OssieDocument.yaml(document)), any());
+    verifyNoMoreInteractions(dispatcher);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testExportEmitsDedicatedEventsWithoutSourceLookup(boolean missing) {
+    NameIdentifier ident = semanticModelIdentifier("sales");
+    if (missing) {
+      when(dispatcher.exportOssieDocument(ident, OssieFormat.YAML))
+          .thenThrow(new NoSuchSemanticModelException("missing model"));
+    } else {
+      when(dispatcher.exportOssieDocument(ident, 
OssieFormat.YAML)).thenReturn(EXPORTED_YAML);
+    }
+    try (Response response = getOssie(semanticModelPath() + "/sales/ossie")) {
+      Assertions.assertEquals(missing ? 404 : 200, response.getStatus());
+    }
+    ArgumentCaptor<BaseEvent> events = 
ArgumentCaptor.forClass(BaseEvent.class);
+    verify(eventBus, times(2)).dispatchEvent(events.capture());
+    Assertions.assertInstanceOf(ExportSemanticModelPreEvent.class, 
events.getAllValues().get(0));
+    Assertions.assertEquals(
+        missing ? ExportSemanticModelFailureEvent.class : 
ExportSemanticModelEvent.class,
+        events.getAllValues().get(1).getClass());
+    verifyNoInteractions(tables, views);
+    verifyNoModelWrites();
+    verify(dispatcher).exportOssieDocument(ident, OssieFormat.YAML);
+    verifyNoMoreInteractions(dispatcher);
   }
 
   private SemanticModelCreateRequest createRequest(

Reply via email to