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(