jerryshao commented on code in PR #13571:
URL: https://github.com/apache/gravitino/pull/13571#discussion_r4227834817


##########
core/src/main/java/org/apache/gravitino/semantic/OssieDocumentConverter.java:
##########
@@ -0,0 +1,785 @@
+/*
+ * 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.semantic;
+
+import static 
org.apache.gravitino.semantic.CustomExtension.GRAVITINO_PROPERTIES_VENDOR;
+import static org.apache.gravitino.semantic.OssieVersion.DEFAULT_VERSION;
+import static 
org.apache.gravitino.semantic.SemanticModel.PROPERTY_OSSIE_VERSION;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.StreamReadFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.requests.SemanticModelCreateRequest;
+import org.apache.gravitino.dto.semantic.SemanticModelDefinitionDTO;
+import org.apache.gravitino.exceptions.IllegalSemanticModelException;
+
+/** Converts between standalone Apache Ossie documents and Gravitino Semantic 
Models. */
+public final class OssieDocumentConverter {
+
+  private static final Set<String> ROOT_PROPERTIES =
+      Set.of(
+          "version",
+          "name",
+          "description",
+          "ai_context",
+          "datasets",
+          "relationships",
+          "metrics",
+          "custom_extensions");
+  private static final Set<String> DATASET_PROPERTIES =
+      Set.of(
+          "name",
+          "source",
+          "primary_key",
+          "unique_keys",
+          "description",
+          "ai_context",
+          "fields",
+          "custom_extensions");
+  private static final Set<String> FIELD_PROPERTIES =
+      Set.of(
+          "name",
+          "expression",
+          "dimension",
+          "label",
+          "description",
+          "datatype",
+          "ai_context",
+          "custom_extensions");
+  private static final Set<String> RELATIONSHIP_PROPERTIES =
+      Set.of("name", "from", "to", "from_columns", "to_columns", "ai_context", 
"custom_extensions");
+  private static final Set<String> METRIC_PROPERTIES =
+      Set.of("name", "expression", "description", "datatype", "ai_context", 
"custom_extensions");
+  private static final Set<String> EXPRESSION_PROPERTIES = Set.of("dialects");
+  private static final Set<String> DIALECT_EXPRESSION_PROPERTIES = 
Set.of("dialect", "expression");
+  private static final Set<String> DIMENSION_PROPERTIES = Set.of("is_time");
+  private static final Set<String> CUSTOM_EXTENSION_PROPERTIES = 
Set.of("vendor_name", "data");
+
+  private static final ObjectMapper JSON_MAPPER = createJsonMapper();
+  private static final ObjectMapper YAML_MAPPER = createYamlMapper();
+
+  private OssieDocumentConverter() {}
+
+  /**
+   * Converts one standalone Apache Ossie YAML or JSON document into a create 
request.
+   *
+   * @param document The standalone Ossie document.
+   * @return The converted Semantic Model create request.
+   * @throws IllegalSemanticModelException If the document cannot be parsed or 
represented by
+   *     Gravitino.
+   */
+  public static SemanticModelCreateRequest importDocument(OssieDocument 
document) {
+    Objects.requireNonNull(document, "document must not be null");
+    ObjectNode root = parseDocument(document);
+    return toCreateRequest(root);
+  }
+
+  /**
+   * Exports one Gravitino Semantic Model as a standalone Apache Ossie 
document.
+   *
+   * @param semanticModel The Semantic Model to export.
+   * @param format The requested serialization format.
+   * @return The serialized Ossie document.
+   * @throws IllegalSemanticModelException If the model cannot be represented 
as a valid Ossie
+   *     document.
+   */
+  public static OssieDocument exportDocument(SemanticModel semanticModel, 
OssieFormat format) {
+    Objects.requireNonNull(semanticModel, "semanticModel must not be null");
+    Objects.requireNonNull(format, "format must not be null");
+
+    ObjectNode definition =
+        JSON_MAPPER.valueToTree(
+            
SemanticModelDefinitionDTO.fromDefinition(semanticModel.definition()));
+    transformNativeDefinition(definition);
+
+    ObjectNode root = JSON_MAPPER.createObjectNode();
+    root.put("version", ossieVersion(semanticModel.properties()));
+    root.put("name", semanticModel.name());
+    if (semanticModel.comment() != null) {
+      root.put("description", semanticModel.comment());
+    }
+    definition.fields().forEachRemaining(entry -> root.set(entry.getKey(), 
entry.getValue()));
+    stashProperties(root, semanticModel.properties());
+
+    // Validate the generated document through the same conversion path used 
for imports.
+    toCreateRequest(root.deepCopy());
+
+    try {
+      if (format == OssieFormat.JSON) {
+        return OssieDocument.json(
+            
JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(root) + "\n");
+      }
+      return OssieDocument.yaml(YAML_MAPPER.writeValueAsString(root));
+    } catch (JsonProcessingException e) {
+      throw new IllegalSemanticModelException(
+          e, "Cannot serialize Apache Ossie document: %s", 
e.getOriginalMessage());
+    }
+  }
+
+  private static ObjectMapper createJsonMapper() {
+    JsonFactory factory =
+        
JsonFactory.builder().enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION).build();
+    return JsonMapper.builder(factory)
+        .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
+        .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)
+        .build()
+        .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+  }
+
+  private static ObjectMapper createYamlMapper() {
+    YAMLFactory factory =
+        YAMLFactory.builder()
+            .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION)
+            .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
+            .build();
+    return new ObjectMapper(factory)
+        .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
+        .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
+        .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)
+        .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+  }
+
+  private static ObjectNode parseDocument(OssieDocument document) {
+    String content = document.content();
+    if (StringUtils.isBlank(content)) {
+      throw new IllegalSemanticModelException("Apache Ossie document must not 
be empty");

Review Comment:
   [Nit] This blank-content guard is now unreachable, and its tests were 
deleted with it.
   
   `importDocument` only takes an `OssieDocument`, and every construction path 
goes through the private constructor, which since this commit rejects blank 
content via `StringUtils.isNotBlank` 
(`api/src/main/java/org/apache/gravitino/semantic/OssieDocument.java:39`). 
`exportDocument` is the only other producer and always emits at least `version` 
and `name`. So `StringUtils.isBlank(content)` here can never be true, and the 
two cases that used to cover it -- `assertInvalid("", "must not be empty")` and 
`assertInvalid(OssieDocument.json(" "), "must not be empty")` -- were removed 
from `TestOssieDocumentConverter` in this commit.
   
   Either drop the branch, or keep it with a short comment saying it is defence 
in depth for a future `OssieDocument` producer. The REST-level case is already 
pinned by 
`TestSemanticModelOperations.testOssieImportRejectsBlankContentBeforeDispatch`, 
so nothing is lost by deleting it.
   
   Verified by: read `OssieDocument`'s constructor and both factories at 
`088ceac`; grepped every caller of `OssieDocumentConverter.importDocument` (the 
`SemanticModelDispatcher` default, two tests) and confirmed all inputs are 
`OssieDocument` values; diffed `TestOssieDocumentConverter` against `baf49af` 
to confirm the two blank assertions were deleted.



##########
server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java:
##########
@@ -162,6 +164,67 @@ public Response createSemanticModel(
     }
   }
 
+  /**
+   * Imports a standalone Apache Ossie YAML or JSON document as a Semantic 
Model.
+   *
+   * <p>The request body is raw document text, not a JSON-encoded string. The 
{@code Content-Type}
+   * header selects the document format; an absent header defaults to YAML. 
Parsing does not fall
+   * back to another format if the document is invalid for the selected parser.
+   *
+   * @param metalake The metalake name.
+   * @param catalog The catalog name.
+   * @param schema The schema name.
+   * @param document The standalone Ossie document.
+   * @param headers The request headers specifying the document's media type.
+   * @return A native response containing the created Semantic Model.
+   */
+  @POST
+  @Path("ossie")
+  @Consumes({
+    MediaType.APPLICATION_JSON,
+    OSSIE_YAML_MEDIA_TYPE,
+    OSSIE_X_YAML_MEDIA_TYPE,
+    OSSIE_TEXT_YAML_MEDIA_TYPE
+  })
+  @Produces(VND_GRAVITINO_V1_JSON)
+  @Timed(name = "import-ossie-semantic-model." + 
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
+  @ResponseMetered(name = "import-ossie-semantic-model", absolute = true)
+  public Response importOssieDocument(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      String document,
+      @Context HttpHeaders headers) {
+    LOG.info(
+        "Received import Apache Ossie Semantic Model request for schema: 
{}.{}.{}",
+        metalake,
+        catalog,
+        schema);
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            OssieDocument ossieDocument =
+                
MediaType.APPLICATION_JSON_TYPE.isCompatible(headers.getMediaType())
+                    ? OssieDocument.json(document)
+                    : OssieDocument.yaml(document);

Review Comment:
   [Question] A blank body now reports a different error `type` than every 
other malformed document -- intended for the client follow-up?
   
   Moving the blank check into `OssieDocument` means this line throws a plain 
`IllegalArgumentException`, so the response carries `type: 
IllegalArgumentException` and the message `content must not be blank`. Every 
other bad document still fails inside the converter as 
`IllegalSemanticModelException`. Both are 400 with `ILLEGAL_ARGUMENTS_CODE` 
(`SemanticModelExceptionHandler` branches on `e instanceof 
IllegalArgumentException`, and `IllegalSemanticModelException` extends it), so 
nothing is wrong today -- `clients/client-java/.../ErrorHandlers.java` has no 
Semantic Model handler yet, so no client reconstructs the exception from `type`.
   
   It only matters once the client import/export follow-up adds one: it would 
have to accept two `type` strings for the same class of failure. If you would 
rather keep one, catching blank content here and throwing 
`IllegalSemanticModelException` would do it; otherwise worth a line in the 
follow-up issue so the handler is written for both.
   
   Verified by: read `SemanticModelExceptionHandler.handle` 
(`server/.../rest/ExceptionHandlers.java:1019-1047`) and confirmed the 
`IllegalArgumentException` branch fires first; read the new 
`testOssieImportRejectsBlankContentBeforeDispatch`, which asserts exactly 
`IllegalArgumentException.class.getSimpleName()`; grepped 
`clients/client-java/.../ErrorHandlers.java` for `SemanticModel` and found no 
handler.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to