diff --git a/openmetadata-service/pom.xml b/openmetadata-service/pom.xml index 3a907a8ae6ea..82b2e90130a9 100644 --- a/openmetadata-service/pom.xml +++ b/openmetadata-service/pom.xml @@ -294,6 +294,10 @@ io.dropwizard.modules dropwizard-web + + com.github.erosb + everit-json-schema + diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java index becbd478d591..983357d9b5c4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java @@ -26,6 +26,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import javax.json.JsonPatch; import javax.validation.Valid; @@ -68,6 +70,7 @@ import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.RestUtil.PutResponse; import org.openmetadata.service.util.ResultList; +import org.openmetadata.service.util.SchemaFieldExtractor; @Path("/v1/metadata/types") @Tag( @@ -466,6 +469,57 @@ public Response addOrUpdateProperty( return response.toResponse(); } + @GET + @Path("/fields/{entityType}") + @Produces(MediaType.APPLICATION_JSON) + public Response getEntityTypeFields( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @PathParam("entityType") String entityType, + @QueryParam("include") @DefaultValue("non-deleted") Include include) { + + try { + Fields fieldsParam = new Fields(Set.of("customProperties")); + Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, include, false); + SchemaFieldExtractor extractor = new SchemaFieldExtractor(); + List fieldsList = + extractor.extractFields(typeEntity, entityType); + return Response.ok(fieldsList).type(MediaType.APPLICATION_JSON).build(); + + } catch (Exception e) { + LOG.error("Error processing schema for entity type: " + entityType, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + "Error processing schema for entity type: " + + entityType + + ". Exception: " + + e.getMessage()) + .build(); + } + } + + @GET + @Path("/customProperties") + @Produces(MediaType.APPLICATION_JSON) + public Response getAllCustomPropertiesByEntityType( + @Context UriInfo uriInfo, @Context SecurityContext securityContext) { + try { + SchemaFieldExtractor extractor = new SchemaFieldExtractor(); + Map> customPropertiesMap = + extractor.extractAllCustomProperties(uriInfo, repository); + return Response.ok(customPropertiesMap).build(); + } catch (Exception e) { + LOG.error("Error fetching custom properties: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + "Error processing schema for entity type: " + + entityType + + ". Exception: " + + e.getMessage()) + .build(); + } + } + private Type getType(CreateType create, String user) { return repository .copy(new Type(), create, user) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java new file mode 100644 index 000000000000..a53c640e1fac --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java @@ -0,0 +1,607 @@ +package org.openmetadata.service.util; + +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.ws.rs.core.UriInfo; +import lombok.extern.slf4j.Slf4j; +import org.everit.json.schema.*; +import org.everit.json.schema.loader.SchemaClient; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.openmetadata.schema.entity.Type; +import org.openmetadata.schema.entity.type.CustomProperty; +import org.openmetadata.schema.type.Include; +import org.openmetadata.sdk.exception.SchemaProcessingException; +import org.openmetadata.service.jdbi3.TypeRepository; + +@Slf4j +public class SchemaFieldExtractor { + + public SchemaFieldExtractor() {} + + public List extractFields(Type typeEntity, String entityType) + throws SchemaProcessingException { + String schemaPath = determineSchemaPath(entityType); + String schemaUri = "classpath:///" + schemaPath; + SchemaClient schemaClient = new CustomSchemaClient(schemaUri); + Map fieldTypesMap = new LinkedHashMap<>(); + Deque processingStack = new ArrayDeque<>(); + Set processedFields = new HashSet<>(); + Schema mainSchema = loadMainSchema(schemaPath, entityType, schemaUri, schemaClient); + extractFieldsFromSchema(mainSchema, "", fieldTypesMap, processingStack, processedFields); + addCustomProperties( + typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields); + return convertMapToFieldList(fieldTypesMap); + } + + public Map> extractAllCustomProperties( + UriInfo uriInfo, TypeRepository repository) { + Map> entityTypeToFields = new HashMap<>(); + List entityTypes = getAllEntityTypes(); + + for (String entityType : entityTypes) { + String schemaPath = determineSchemaPath(entityType); + String schemaUri = "classpath:///" + schemaPath; + SchemaClient schemaClient = new CustomSchemaClient(schemaUri); + EntityUtil.Fields fieldsParam = new EntityUtil.Fields(Set.of("customProperties")); + Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, Include.ALL, false); + Map fieldTypesMap = new LinkedHashMap<>(); + Set processedFields = new HashSet<>(); + Deque processingStack = new ArrayDeque<>(); + addCustomProperties( + typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields); + entityTypeToFields.put(entityType, convertMapToFieldList(fieldTypesMap)); + } + + return entityTypeToFields; + } + + public static List getAllEntityTypes() { + List entityTypes = new ArrayList<>(); + try { + String schemaDirectory = "json/schema/entity/"; + Enumeration resources = + SchemaFieldExtractor.class.getClassLoader().getResources(schemaDirectory); + while (resources.hasMoreElements()) { + URL resourceUrl = resources.nextElement(); + Path schemaDirPath = Paths.get(resourceUrl.toURI()); + + Files.walk(schemaDirPath) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".json")) + .forEach( + path -> { + try (InputStream is = Files.newInputStream(path)) { + JSONObject jsonSchema = new JSONObject(new JSONTokener(is)); + // Check if the schema is an entity type + if (isEntityType(jsonSchema)) { + String fileName = path.getFileName().toString(); + String entityType = + fileName.substring(0, fileName.length() - 5); // Remove ".json" + entityTypes.add(entityType); + LOG.debug("Found entity type: {}", entityType); + } + } catch (Exception e) { + LOG.error("Error reading schema file {}: {}", path, e.getMessage()); + } + }); + } + } catch (Exception e) { + LOG.error("Error scanning schema directory: {}", e.getMessage()); + } + return entityTypes; + } + + private static boolean isEntityType(JSONObject jsonSchema) { + return "@om-entity-type".equals(jsonSchema.optString("$comment")); + } + + private Schema loadMainSchema( + String schemaPath, String entityType, String schemaUri, SchemaClient schemaClient) + throws SchemaProcessingException { + InputStream schemaInputStream = getClass().getClassLoader().getResourceAsStream(schemaPath); + if (schemaInputStream == null) { + LOG.error("Schema file not found at path: {}", schemaPath); + throw new SchemaProcessingException( + "Schema file not found for entity type: " + entityType, + SchemaProcessingException.ErrorType.RESOURCE_NOT_FOUND); + } + + JSONObject rawSchema = new JSONObject(new JSONTokener(schemaInputStream)); + SchemaLoader schemaLoader = + SchemaLoader.builder() + .schemaJson(rawSchema) + .resolutionScope(schemaUri) + .schemaClient(schemaClient) + .build(); + + try { + Schema schema = schemaLoader.load().build(); + LOG.debug("Schema '{}' loaded successfully.", schemaPath); + return schema; + } catch (Exception e) { + LOG.error("Error loading schema '{}': {}", schemaPath, e.getMessage()); + throw new SchemaProcessingException( + "Error loading schema '" + schemaPath + "': " + e.getMessage(), + SchemaProcessingException.ErrorType.OTHER); + } + } + + private void extractFieldsFromSchema( + Schema schema, + String parentPath, + Map fieldTypesMap, + Deque processingStack, + Set processedFields) { + if (processingStack.contains(schema)) { + LOG.debug( + "Detected cyclic reference at path '{}'. Skipping further processing of this schema.", + parentPath); + return; + } + + processingStack.push(schema); + try { + if (schema instanceof ObjectSchema objectSchema) { + for (Map.Entry propertyEntry : + objectSchema.getPropertySchemas().entrySet()) { + String fieldName = propertyEntry.getKey(); + Schema fieldSchema = propertyEntry.getValue(); + String fullFieldName = parentPath.isEmpty() ? fieldName : parentPath + "." + fieldName; + + if (processedFields.contains(fullFieldName)) { + LOG.debug( + "Field '{}' has already been processed. Skipping to prevent duplication.", + fullFieldName); + continue; + } + + LOG.debug("Processing field '{}'", fullFieldName); + + if (fieldSchema instanceof ReferenceSchema referenceSchema) { + handleReferenceSchema( + referenceSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } else if (fieldSchema instanceof ArraySchema arraySchema) { + handleArraySchema( + arraySchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } else { + String fieldType = mapSchemaTypeToSimpleType(fieldSchema); + fieldTypesMap.putIfAbsent(fullFieldName, fieldType); + processedFields.add(fullFieldName); + LOG.debug("Added field '{}', Type: '{}'", fullFieldName, fieldType); + // Recursively process nested objects or arrays + if (fieldSchema instanceof ObjectSchema || fieldSchema instanceof ArraySchema) { + extractFieldsFromSchema( + fieldSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } + } + } + } else if (schema instanceof ArraySchema arraySchema) { + handleArraySchema(arraySchema, parentPath, fieldTypesMap, processingStack, processedFields); + } else { + String fieldType = mapSchemaTypeToSimpleType(schema); + fieldTypesMap.putIfAbsent(parentPath, fieldType); + LOG.debug("Added field '{}', Type: '{}'", parentPath, fieldType); + } + } finally { + processingStack.pop(); + } + } + + private void handleReferenceSchema( + ReferenceSchema referenceSchema, + String fullFieldName, + Map fieldTypesMap, + Deque processingStack, + Set processedFields) { + + String refUri = referenceSchema.getReferenceValue(); + String referenceType = determineReferenceType(refUri); + + if (referenceType != null) { + fieldTypesMap.putIfAbsent(fullFieldName, referenceType); + processedFields.add(fullFieldName); + LOG.debug("Added field '{}', Type: '{}'", fullFieldName, referenceType); + if (referenceType.startsWith("array<") && referenceType.endsWith(">")) { + Schema itemSchema = + referenceSchema.getReferredSchema() instanceof ArraySchema + ? ((ArraySchema) referenceSchema.getReferredSchema()).getAllItemSchema() + : referenceSchema.getReferredSchema(); + extractFieldsFromSchema( + itemSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } else if (!isPrimitiveType(referenceType)) { + Schema referredSchema = referenceSchema.getReferredSchema(); + extractFieldsFromSchema( + referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } + } else { + fieldTypesMap.putIfAbsent(fullFieldName, "object"); + processedFields.add(fullFieldName); + LOG.debug("Added field '{}', Type: 'object'", fullFieldName); + extractFieldsFromSchema( + referenceSchema.getReferredSchema(), + fullFieldName, + fieldTypesMap, + processingStack, + processedFields); + } + } + + private void handleArraySchema( + ArraySchema arraySchema, + String fullFieldName, + Map fieldTypesMap, + Deque processingStack, + Set processedFields) { + + Schema itemsSchema = arraySchema.getAllItemSchema(); + + if (itemsSchema instanceof ReferenceSchema itemsReferenceSchema) { + String itemsRefUri = itemsReferenceSchema.getReferenceValue(); + String itemsReferenceType = determineReferenceType(itemsRefUri); + + if (itemsReferenceType != null) { + String arrayFieldType = "array<" + itemsReferenceType + ">"; + fieldTypesMap.putIfAbsent(fullFieldName, arrayFieldType); + processedFields.add(fullFieldName); + LOG.debug("Added field '{}', Type: '{}'", fullFieldName, arrayFieldType); + Schema referredItemsSchema = itemsReferenceSchema.getReferredSchema(); + extractFieldsFromSchema( + referredItemsSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + return; + } + } + String arrayType = mapSchemaTypeToSimpleType(itemsSchema); + fieldTypesMap.putIfAbsent(fullFieldName, "array<" + arrayType + ">"); + processedFields.add(fullFieldName); + LOG.debug("Added field '{}', Type: 'array<{}>'", fullFieldName, arrayType); + + if (itemsSchema instanceof ObjectSchema || itemsSchema instanceof ArraySchema) { + extractFieldsFromSchema( + itemsSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } + } + + private void addCustomProperties( + Type typeEntity, + String schemaUri, + SchemaClient schemaClient, + Map fieldTypesMap, + Deque processingStack, + Set processedFields) { + if (typeEntity == null || typeEntity.getCustomProperties() == null) { + return; + } + + for (CustomProperty customProperty : typeEntity.getCustomProperties()) { + String propertyName = customProperty.getName(); + String propertyType = customProperty.getPropertyType().getName(); + String fullFieldName = propertyName; // No parent path for custom properties + + LOG.debug("Processing custom property '{}'", fullFieldName); + + if (isEntityReferenceList(propertyType)) { + String referenceType = "array"; + fieldTypesMap.putIfAbsent(fullFieldName, referenceType); + processedFields.add(fullFieldName); + LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType); + + Schema itemSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient); + if (itemSchema != null) { + extractFieldsFromSchema( + itemSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } else { + LOG.warn( + "Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.", + fullFieldName); + } + } else if (isEntityReference(propertyType)) { + String referenceType = "entityReference"; + fieldTypesMap.putIfAbsent(fullFieldName, referenceType); + processedFields.add(fullFieldName); + LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType); + + Schema referredSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient); + if (referredSchema != null) { + extractFieldsFromSchema( + referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields); + } else { + LOG.warn( + "Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.", + fullFieldName); + } + } else { + fieldTypesMap.putIfAbsent(fullFieldName, propertyType); + processedFields.add(fullFieldName); + LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, propertyType); + } + } + } + + private List convertMapToFieldList(Map fieldTypesMap) { + List fieldsList = new ArrayList<>(); + for (Map.Entry entry : fieldTypesMap.entrySet()) { + fieldsList.add(new FieldDefinition(entry.getKey(), entry.getValue())); + } + return fieldsList; + } + + private boolean isEntityReferenceList(String propertyType) { + return "entityReferenceList".equalsIgnoreCase(propertyType); + } + + private boolean isEntityReference(String propertyType) { + return "entityReference".equalsIgnoreCase(propertyType); + } + + private Schema resolveSchemaByType(String typeName, String schemaUri, SchemaClient schemaClient) { + String referencePath = determineReferencePath(typeName); + try { + return loadSchema(referencePath, schemaUri, schemaClient); + } catch (SchemaProcessingException e) { + LOG.error("Failed to load schema for type '{}': {}", typeName, e.getMessage()); + return null; + } + } + + private Schema loadSchema(String schemaPath, String schemaUri, SchemaClient schemaClient) + throws SchemaProcessingException { + InputStream schemaInputStream = getClass().getClassLoader().getResourceAsStream(schemaPath); + if (schemaInputStream == null) { + LOG.error("Schema file not found at path: {}", schemaPath); + throw new SchemaProcessingException( + "Schema file not found for path: " + schemaPath, + SchemaProcessingException.ErrorType.RESOURCE_NOT_FOUND); + } + + JSONObject rawSchema = new JSONObject(new JSONTokener(schemaInputStream)); + SchemaLoader schemaLoader = + SchemaLoader.builder() + .schemaJson(rawSchema) + .resolutionScope(schemaUri) // Base URI for resolving $ref + .schemaClient(schemaClient) + .build(); + + try { + Schema schema = schemaLoader.load().build(); + LOG.debug("Schema '{}' loaded successfully.", schemaPath); + return schema; + } catch (Exception e) { + LOG.error("Error loading schema '{}': {}", schemaPath, e.getMessage()); + throw new SchemaProcessingException( + "Error loading schema '" + schemaPath + "': " + e.getMessage(), + SchemaProcessingException.ErrorType.OTHER); + } + } + + private String determineReferenceType(String refUri) { + // Pattern to extract the definition name if present + Pattern definitionPattern = Pattern.compile("^(?:.*/)?basic\\.json#/definitions/([\\w-]+)$"); + Matcher matcher = definitionPattern.matcher(refUri); + if (matcher.find()) { + String definition = matcher.group(1); + return switch (definition) { + case "duration" -> "duration"; + case "markdown" -> "markdown"; + case "timestamp" -> "timestamp"; + case "integer" -> "integer"; + case "number" -> "number"; + case "string" -> "string"; + case "uuid" -> "uuid"; + case "email" -> "email"; + case "href" -> "href"; + case "timeInterval" -> "timeInterval"; + case "date" -> "date"; + case "dateTime" -> "dateTime"; + case "time" -> "time"; + case "date-cp" -> "date-cp"; + case "dateTime-cp" -> "dateTime-cp"; + case "time-cp" -> "time-cp"; + case "enum" -> "enum"; + case "enumWithDescriptions" -> "enumWithDescriptions"; + case "timezone" -> "timezone"; + case "entityLink" -> "entityLink"; + case "entityName" -> "entityName"; + case "testCaseEntityName" -> "testCaseEntityName"; + case "fullyQualifiedEntityName" -> "fullyQualifiedEntityName"; + case "sqlQuery" -> "sqlQuery"; + case "sqlFunction" -> "sqlFunction"; + case "expression" -> "expression"; + case "jsonSchema" -> "jsonSchema"; + case "entityExtension" -> "entityExtension"; + case "providerType" -> "providerType"; + case "componentConfig" -> "componentConfig"; + case "status" -> "status"; + case "sourceUrl" -> "sourceUrl"; + case "style" -> "style"; + default -> { + LOG.warn("Unrecognized definition '{}' in refUri '{}'", definition, refUri); + yield "object"; + } + }; + } + + // Existing file-based mappings + if (refUri.matches(".*basic\\.json$")) { + return "uuid"; + } + if (refUri.matches(".*entityReference\\.json(?:#.*)?$")) { + return "entityReference"; + } + if (refUri.matches(".*entityReferenceList\\.json(?:#.*)?$")) { + return "array"; + } + if (refUri.matches(".*tagLabel\\.json(?:#.*)?$")) { + return "tagLabel"; + } + if (refUri.matches(".*fullyQualifiedEntityName\\.json(?:#.*)?$")) { + return "fullyQualifiedEntityName"; + } + if (refUri.matches(".*entityVersion\\.json(?:#.*)?$")) { + return "entityVersion"; + } + if (refUri.matches(".*markdown\\.json(?:#.*)?$")) { + return "markdown"; + } + if (refUri.matches(".*timestamp\\.json(?:#.*)?$")) { + return "timestamp"; + } + if (refUri.matches(".*href\\.json(?:#.*)?$")) { + return "href"; + } + if (refUri.matches(".*duration\\.json(?:#.*)?$")) { + return "duration"; + } + return null; + } + + private String mapSchemaTypeToSimpleType(Schema schema) { + if (schema == null) { + LOG.debug("Mapping type: null -> 'object'"); + return "object"; + } + if (schema instanceof StringSchema) { + LOG.debug("Mapping schema instance '{}' to 'string'", schema.getClass().getSimpleName()); + return "string"; + } else if (schema instanceof NumberSchema numberSchema) { + if (numberSchema.requiresInteger()) { + LOG.debug("Mapping schema instance '{}' to 'integer'", schema.getClass().getSimpleName()); + return "integer"; + } else { + LOG.debug("Mapping schema instance '{}' to 'number'", schema.getClass().getSimpleName()); + return "number"; + } + } else if (schema instanceof BooleanSchema) { + LOG.debug("Mapping schema instance '{}' to 'boolean'", schema.getClass().getSimpleName()); + return "boolean"; + } else if (schema instanceof ObjectSchema) { + LOG.debug("Mapping schema instance '{}' to 'object'", schema.getClass().getSimpleName()); + return "object"; + } else if (schema instanceof ArraySchema) { + LOG.debug("Mapping schema instance '{}' to 'array'", schema.getClass().getSimpleName()); + return "array"; + } else if (schema instanceof NullSchema) { + LOG.debug("Mapping schema instance '{}' to 'null'", schema.getClass().getSimpleName()); + return "null"; + } else { + LOG.debug( + "Mapping unknown schema instance '{}' to 'string'", schema.getClass().getSimpleName()); + return "string"; + } + } + + private boolean isPrimitiveType(String type) { + return type.equals("string") + || type.equals("integer") + || type.equals("number") + || type.equals("boolean") + || type.equals("uuid") + || // Treat 'uuid' as a primitive type + type.equals("timestamp") + || type.equals("href") + || type.equals("duration") + || type.equals("date") + || type.equals("dateTime") + || type.equals("time") + || type.equals("date-cp") + || type.equals("dateTime-cp") + || type.equals("time-cp") + || type.equals("enum") + || type.equals("enumWithDescriptions") + || type.equals("timezone") + || type.equals("entityLink") + || type.equals("entityName") + || type.equals("testCaseEntityName") + || type.equals("fullyQualifiedEntityName") + || type.equals("sqlQuery") + || type.equals("sqlFunction") + || type.equals("expression") + || type.equals("jsonSchema") + || type.equals("entityExtension") + || type.equals("providerType") + || type.equals("componentConfig") + || type.equals("status") + || type.equals("sourceUrl") + || type.equals("style"); + } + + private String determineReferencePath(String typeName) { + String baseSchemaDirectory = "json/schema/entity/"; + String schemaFileName = typeName + ".json"; + return baseSchemaDirectory + schemaFileName; + } + + private String determineSchemaPath(String entityType) { + String subdirectory = getEntitySubdirectory(entityType); + return "json/schema/entity/" + subdirectory + "/" + entityType + ".json"; + } + + private String getEntitySubdirectory(String entityType) { + Map entityTypeToSubdirectory = + Map.of( + "dashboard", "data", + "table", "data", + "pipeline", "services", + "votes", "data"); + return entityTypeToSubdirectory.getOrDefault(entityType, "data"); + } + + @Slf4j + private static class CustomSchemaClient implements SchemaClient { + private final String baseUri; + + public CustomSchemaClient(String baseUri) { + this.baseUri = baseUri; + } + + @Override + public InputStream get(String url) { + LOG.debug("SchemaClient: Resolving URL '{}' against base URI '{}'", url, baseUri); + String resourcePath = mapUrlToResourcePath(url); + LOG.debug("SchemaClient: Loading resource from path '{}'", resourcePath); + InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath); + if (is == null) { + LOG.error("Resource not found: {}", resourcePath); + throw new RuntimeException("Resource not found: " + resourcePath); + } + return is; + } + + private String mapUrlToResourcePath(String url) { + if (url.startsWith("https://open-metadata.org/schema/")) { + String relativePath = url.substring("https://open-metadata.org/schema/".length()); + return "json/schema/" + relativePath; + } else { + throw new RuntimeException("Unsupported URL: " + url); + } + } + } + + @lombok.Getter + @lombok.Setter + public static class FieldDefinition { + private String name; + private String type; + + public FieldDefinition(String name, String type) { + this.name = name; + this.type = type; + } + } +} diff --git a/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java b/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java new file mode 100644 index 000000000000..5ecf05101e40 --- /dev/null +++ b/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java @@ -0,0 +1,24 @@ +package org.openmetadata.sdk.exception; + +import lombok.Getter; + +@Getter +public class SchemaProcessingException extends Exception { + public enum ErrorType { + RESOURCE_NOT_FOUND, + UNSUPPORTED_URL, + OTHER + } + + private final ErrorType errorType; + + public SchemaProcessingException(String message, ErrorType errorType) { + super(message); + this.errorType = errorType; + } + + public SchemaProcessingException(String message, Throwable cause, ErrorType errorType) { + super(message, cause); + this.errorType = errorType; + } +} diff --git a/pom.xml b/pom.xml index 9be45980f522..54d7c4207c71 100644 --- a/pom.xml +++ b/pom.xml @@ -156,6 +156,7 @@ 1.2.13 1.2.13 2.9.0 + 1.14.4 @@ -517,7 +518,11 @@ picocli ${picocli.version} - + + com.github.erosb + everit-json-schema + ${everit.version} + org.eclipse.jetty