From d1d4568dfaeac6d86969601d37d7aba770233913 Mon Sep 17 00:00:00 2001 From: yyin-talend Date: Wed, 24 Dec 2025 10:37:59 +0800 Subject: [PATCH 1/5] Add Json Schema --- .../server/front/model/JsonEntryModel.java | 152 ++++++++++++ .../server/front/model/JsonSchemaModel.java | 142 +++++++++++ .../component-server/pom.xml | 11 +- .../server/front/JsonSchemaTest.java | 234 ++++++++++++++++++ 4 files changed, 534 insertions(+), 5 deletions(-) create mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java create mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java new file mode 100644 index 0000000000000..711d87c44110a --- /dev/null +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java @@ -0,0 +1,152 @@ +package org.talend.sdk.component.server.front.model; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.json.JsonObject; +import javax.json.JsonValue; +import lombok.Getter; +import lombok.Setter; + +public class JsonEntryModel { + + private final JsonObject jsonEntry; + + @Getter + private final boolean isMetadata; + + /** + * The name of this entry. + */ + @Setter + @Getter + private String name; + + /** + * The raw name of this entry. + */ + @Setter + @Getter + private String rawName; + + /** + * Type of the entry, this determine which other fields are populated. + */ + @Setter + @Getter + private JsonSchemaModel.Type type; + + /** + * Is this entry nullable or always valued. + */ + @Setter + @Getter + private boolean nullable; + + /** + * Is this entry can be in error. + */ + private boolean errorCapable; + + /** + * Is this entry a metadata entry. + */ + @Setter + @Getter + private boolean metadata; + + /** + * Default value for this entry. + */ + @Setter + @Getter + private Object defaultValue; + + /** + * For type == record, the element type. + */ + @Getter + @Setter + private JsonSchemaModel elementSchema; + + /** + * Allows to associate to this field a comment - for doc purposes, no use in the runtime. + */ + @Setter + @Getter + private String comment; + + @Setter + @Getter + private boolean valid; + + /** + * metadata + */ + @Setter + @Getter + private Map props = new LinkedHashMap<>(0); + + JsonEntryModel(JsonObject jsonEntry, boolean isMetadata) { + this.jsonEntry = jsonEntry; + this.isMetadata = isMetadata; + this.type = JsonSchemaModel.Type.valueOf(jsonEntry.getString("type")); + this.props = parseProps(jsonEntry.getJsonObject("props")); + + // Parse element schema if present + this.elementSchema = jsonEntry.containsKey("elementSchema") + ? new JsonSchemaModel(jsonEntry.getJsonObject("elementSchema").toString()) + : null; + } + + private Map parseProps(JsonObject propsObj) { + if (propsObj == null) return Collections.emptyMap(); + + Map result = new HashMap<>(); + propsObj.forEach((key, value) -> + result.put(key, value.getValueType() == JsonValue.ValueType.STRING + ? ((javax.json.JsonString) value).getString() + : value.toString())); + return result; + } + + public String getName() { + return jsonEntry.getString("name"); + } + + public String getRawName() { + return jsonEntry.getString("rawName", getName()); + } + + public String getOriginalFieldName() { + return getRawName() != null ? getRawName() : getName(); + } + + public boolean isNullable() { + return jsonEntry.getBoolean("nullable", true); + } + + public boolean isErrorCapable() { + return jsonEntry.getBoolean("errorCapable", false); + } + + public boolean isValid() { + return jsonEntry.getBoolean("valid", true); + } + + public T getDefaultValue() { + return jsonEntry.containsKey("defaultValue") + ? (T) jsonEntry.get("defaultValue") + : null; + } + + public String getComment() { + return jsonEntry.getString("comment", null); + } + + public String getProp(String property) { + return props.get(property); + } + +} diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java new file mode 100644 index 0000000000000..5890ccec51425 --- /dev/null +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java @@ -0,0 +1,142 @@ +package org.talend.sdk.component.server.front.model; + +import java.io.StringReader; +import java.math.BigDecimal; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import java.util.stream.Collectors; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import lombok.Getter; +import lombok.Setter; +//import org.json.JSONObject; + +public class JsonSchemaModel { + + @Getter + @Setter + private Type type; + + private final JsonObject jsonSchema; + + @Getter + private JsonSchemaModel elementSchema; + + @Getter + @Setter + private List entries = new ArrayList(); + + @Getter + private List metadataEntries = new ArrayList(); + ; + + @Getter + @Setter + private Map props = new HashMap(); + + @Getter + private Map entryMap = new HashMap<>(); + + public JsonSchemaModel(String jsonString) { + try (JsonReader reader = Json.createReader(new StringReader(jsonString))) { + this.jsonSchema = reader.readObject(); + } + + this.type = Type.valueOf(jsonSchema.getString("type")); + this.props = parseProps(jsonSchema.getJsonObject("props")); + + // Parse entries + this.entries = parseEntries(jsonSchema.getJsonArray("entries"), false); + this.metadataEntries = parseEntries(jsonSchema.getJsonArray("metadata"), true); + + // Build entry map + this.entryMap = new HashMap<>(); + getEntries().forEach(e -> entryMap.put(e.getName(), e)); + + // Parse element schema for ARRAY types + this.elementSchema = jsonSchema.containsKey("elementSchema") + ? new JsonSchemaModel(jsonSchema.getJsonObject("elementSchema").toString()) + : null; + } + + private List parseEntries(JsonArray jsonArray, boolean isMetadata) { + if (jsonArray == null) return Collections.emptyList(); + + return jsonArray.stream() + .map(JsonValue::asJsonObject) + .map(obj -> new JsonEntryModel(obj, isMetadata)) + .collect(Collectors.toList()); + } + + private Map parseProps(JsonObject propsObj) { + if (propsObj == null) return Collections.emptyMap(); + + Map result = new HashMap<>(); + propsObj.forEach((key, value) -> + result.put(key, value.getValueType() == JsonValue.ValueType.STRING + ? ((javax.json.JsonString) value).getString() + : value.toString())); + return result; + } + + public void setEntryMap(Map entryMap) { + this.entryMap = entryMap; + } + + public void setElementSchema(JsonSchemaModel elementSchema) { + this.elementSchema = elementSchema; + } + + public enum Type { + + RECORD(new Class[] { Record.class }), + ARRAY(new Class[] { Collection.class }), + STRING(new Class[] { String.class, Object.class }), + BYTES(new Class[] { byte[].class, Byte[].class }), + INT(new Class[] { Integer.class }), + LONG(new Class[] { Long.class }), + FLOAT(new Class[] { Float.class }), + DOUBLE(new Class[] { Double.class }), + BOOLEAN(new Class[] { Boolean.class }), + DATETIME(new Class[] { Long.class, Date.class, Temporal.class }), + DECIMAL(new Class[] { BigDecimal.class }); + + /** + * All compatibles Java classes + */ + private final Class[] classes; + + Type(final Class[] classes) { + this.classes = classes; + } + + /** + * Check if input can be affected to an entry of this type. + * + * @param input : object. + * + * @return true if input is null or ok. + */ + public boolean isCompatible(final Object input) { + if (input == null) { + return true; + } + for (final Class clazz : classes) { + if (clazz.isInstance(input)) { + return true; + } + } + return false; + } + } +} diff --git a/component-server-parent/component-server/pom.xml b/component-server-parent/component-server/pom.xml index b923c9b166fe9..396c76c70503b 100644 --- a/component-server-parent/component-server/pom.xml +++ b/component-server-parent/component-server/pom.xml @@ -63,11 +63,6 @@ component-runtime-design-extension ${project.version} - - org.talend.sdk.component - component-server-api - ${project.version} - org.talend.sdk.component vault-client @@ -191,6 +186,12 @@ ${project.version} test + + org.talend.sdk.component + component-server-api + ${project.version} + test + org.talend.sdk.component component-form-core diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java new file mode 100644 index 0000000000000..93433cc4aa6fb --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java @@ -0,0 +1,234 @@ +package org.talend.sdk.component.server.front; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import static org.talend.sdk.component.api.record.Schema.Type.LONG; +import static org.talend.sdk.component.api.record.Schema.Type.RECORD; +import static org.talend.sdk.component.api.record.Schema.Type.STRING; +import static org.junit.jupiter.api.Assertions.*; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import org.junit.jupiter.api.Test; + +import org.talend.sdk.component.api.record.Schema; +import org.talend.sdk.component.runtime.record.SchemaImpl; +import org.talend.sdk.component.server.front.model.JsonEntryModel; +import org.talend.sdk.component.server.front.model.JsonSchemaModel; + +class JsonSchemaTest { + + private final Jsonb jsonb = JsonbBuilder.create(); + + @Test + void shouldSerializeSchemaImplAndDeserializeToJsonSchemaModel() { + // given + final Schema schema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("id") + .withType(STRING) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field2") + .withType(LONG) + .withNullable(false) + .withComment("field2 comment") + .build()) + .withProp("namespace", "test") + .build(); + + // when: serialize SchemaImpl + String json = jsonb.toJson(schema); + + // then: sanity check JSON + assertTrue(json.contains("\"type\":\"RECORD\"")); + assertTrue(json.contains("\"entries\"")); + + // when: deserialize into JsonSchemaModel + JsonSchemaModel model = new JsonSchemaModel(json); + + // then + assertEquals(JsonSchemaModel.Type.RECORD, model.getType()); + assertEquals("test", model.getProps().get("namespace")); + + assertEquals(2, model.getEntries().size()); + assertEquals("id", model.getEntries().get(0).getName()); + + // entryMap built correctly + assertTrue(model.getEntryMap().containsKey("id")); + } + + @Test + void shouldParseArrayEntryElementSchema() { + Schema.Entry nameEntry = new SchemaImpl.EntryImpl.BuilderImpl() + .withName("name") + .withNullable(true) + .withType(Schema.Type.STRING) + .build(); + Schema.Entry ageEntry = new SchemaImpl.EntryImpl.BuilderImpl() + .withName("age") + .withNullable(true) + .withType(Schema.Type.INT) + .build(); + Schema customerSchema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.RECORD) + .withEntry(nameEntry) + .withEntry(ageEntry) + .build(); + + final Schema schema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.ARRAY) // + .withElementSchema(customerSchema) + .withProp("namespace", "test") + .build(); + + String json = jsonb.toJson(schema); + JsonSchemaModel model = new JsonSchemaModel(json); + + JsonSchemaModel innerSchema = model.getElementSchema(); + + assertEquals(JsonSchemaModel.Type.ARRAY, model.getType()); + assertNotNull(innerSchema); + assertEquals(JsonSchemaModel.Type.RECORD, innerSchema.getType()); + //check entry name + final List entryNames = innerSchema.getEntries().stream().map(JsonEntryModel::getName) + .toList(); + Assertions.assertEquals(2, entryNames.size()); + Assertions.assertTrue(entryNames.contains("name")); + Assertions.assertTrue(entryNames.contains("age")); + + //check entry type + final List entryTypes = innerSchema.getEntries().stream().map(JsonEntryModel::getType) + .toList(); + Assertions.assertTrue(entryTypes.contains(JsonSchemaModel.Type.INT)); + Assertions.assertTrue(entryTypes.contains(JsonSchemaModel.Type.STRING)); + } + + + @Test + void shouldMarkMetadataEntries() { + Schema.Entry meta1 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("meta1") // + .withType(Schema.Type.INT) // + .withMetadata(true) // + .build(); + + Schema.Entry meta2 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("meta2") // + .withType(Schema.Type.STRING) // + .withMetadata(true) // + .withNullable(true) // + .build(); + + Schema.Entry data1 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("data1") // + .withType(Schema.Type.INT) // + .build(); + Schema.Entry data2 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("data2") // + .withType(Schema.Type.STRING) // + .withNullable(true) // + .build(); + + Schema schema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.RECORD) // + .withEntry(data1) // + .withEntry(meta1) // + .withEntry(data2) // + .withEntry(meta2) // + .build(); + + String json = jsonb.toJson(schema); + JsonSchemaModel model = new JsonSchemaModel(json); + + JsonEntryModel metaEntry = model.getMetadataEntries().get(0); + + //check entry name + final List entryNames = model.getEntries().stream().map(JsonEntryModel::getName) + .toList(); + Assertions.assertEquals(2, entryNames.size()); + Assertions.assertTrue(entryNames.contains(data1.getName())); + Assertions.assertTrue(entryNames.contains(data2.getName())); + Assertions.assertEquals(4, schema.getAllEntries().count()); + + //check entry type + final List entryTypes = model.getEntries().stream().map(JsonEntryModel::getType) + .map(JsonSchemaModel.Type::name) + .toList(); + Assertions.assertEquals(2, entryTypes.size()); + Assertions.assertTrue(entryTypes.contains(data1.getType().name())); + Assertions.assertTrue(entryTypes.contains(data2.getType().name())); + + //check meta name + final List metaEntryNames = model.getMetadataEntries().stream().map(JsonEntryModel::getName) + .toList(); + Assertions.assertEquals(2, metaEntryNames.size()); + Assertions.assertTrue(metaEntryNames.contains(meta1.getName())); + Assertions.assertTrue(metaEntryNames.contains(meta2.getName())); + + //check meta type + final List metaEntryTypes = model.getMetadataEntries().stream().map(JsonEntryModel::getType) + .map(JsonSchemaModel.Type::name) + .toList(); + Assertions.assertEquals(2, metaEntryTypes.size()); + Assertions.assertTrue(metaEntryTypes.contains(meta1.getType().name())); + Assertions.assertTrue(metaEntryTypes.contains(meta2.getType().name())); + + assertTrue(metaEntry.isMetadata()); + assertEquals("meta1", metaEntry.getName()); + } + + @Test + void shouldParseEntryProps() { + Schema schema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.RECORD) + //.type(Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field") + .withType(Schema.Type.STRING) + .withProp("format", "email") + .build()) + .build(); + + String json = jsonb.toJson(schema); + JsonSchemaModel model = new JsonSchemaModel(json); + + JsonEntryModel entry = model.getEntries().get(0); + + assertEquals("email", entry.getProps().get("format")); + } + + @Test + void shouldFailForInvalidEntryType() { + String invalidJson = """ + { + "type": "RECORD", + "entries": [ + { "name": "x", "type": "INVALID" } + ] + } + """; + + assertThrows(IllegalArgumentException.class, + () -> new JsonSchemaModel(invalidJson)); + } + + + @Test + void shouldFailForInvalidSchemaType() { + String invalidJson = """ + { + "type": "NOT_VALID", + "entries": [] + } + """; + + assertThrows(IllegalArgumentException.class, + () -> new JsonSchemaModel(invalidJson)); + } + +} + + From 53bb857826e08f14ebacf4451068df526cfd47d4 Mon Sep 17 00:00:00 2001 From: yyin-talend Date: Wed, 24 Dec 2025 11:06:01 +0800 Subject: [PATCH 2/5] Add Json Schema --- .../server/front/model/JsonEntryModel.java | 27 ++++++++++-- .../server/front/model/JsonSchemaModel.java | 44 ++++++++++++------- .../component-server/pom.xml | 1 - .../server/front/JsonSchemaTest.java | 17 ++++++- 4 files changed, 67 insertions(+), 22 deletions(-) diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java index 711d87c44110a..6671932d5b1b7 100644 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java @@ -1,11 +1,28 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front.model; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; + import javax.json.JsonObject; import javax.json.JsonValue; + import lombok.Getter; import lombok.Setter; @@ -88,7 +105,7 @@ public class JsonEntryModel { @Getter private Map props = new LinkedHashMap<>(0); - JsonEntryModel(JsonObject jsonEntry, boolean isMetadata) { + JsonEntryModel(final JsonObject jsonEntry, final boolean isMetadata) { this.jsonEntry = jsonEntry; this.isMetadata = isMetadata; this.type = JsonSchemaModel.Type.valueOf(jsonEntry.getString("type")); @@ -100,8 +117,10 @@ public class JsonEntryModel { : null; } - private Map parseProps(JsonObject propsObj) { - if (propsObj == null) return Collections.emptyMap(); + private Map parseProps(final JsonObject propsObj) { + if (propsObj == null) { + return Collections.emptyMap(); + } Map result = new HashMap<>(); propsObj.forEach((key, value) -> @@ -145,7 +164,7 @@ public String getComment() { return jsonEntry.getString("comment", null); } - public String getProp(String property) { + public String getProp(final String property) { return props.get(property); } diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java index 5890ccec51425..5a083b6a34747 100644 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front.model; import java.io.StringReader; @@ -10,16 +25,17 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import java.util.stream.Collectors; + import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonValue; + import lombok.Getter; import lombok.Setter; -//import org.json.JSONObject; + public class JsonSchemaModel { @@ -29,6 +45,7 @@ public class JsonSchemaModel { private final JsonObject jsonSchema; + @Setter @Getter private JsonSchemaModel elementSchema; @@ -45,9 +62,10 @@ public class JsonSchemaModel { private Map props = new HashMap(); @Getter + @Setter private Map entryMap = new HashMap<>(); - public JsonSchemaModel(String jsonString) { + public JsonSchemaModel(final String jsonString) { try (JsonReader reader = Json.createReader(new StringReader(jsonString))) { this.jsonSchema = reader.readObject(); } @@ -69,8 +87,10 @@ public JsonSchemaModel(String jsonString) { : null; } - private List parseEntries(JsonArray jsonArray, boolean isMetadata) { - if (jsonArray == null) return Collections.emptyList(); + private List parseEntries(final JsonArray jsonArray, final boolean isMetadata) { + if (jsonArray == null) { + return Collections.emptyList(); + } return jsonArray.stream() .map(JsonValue::asJsonObject) @@ -78,8 +98,10 @@ private List parseEntries(JsonArray jsonArray, boolean isMetadat .collect(Collectors.toList()); } - private Map parseProps(JsonObject propsObj) { - if (propsObj == null) return Collections.emptyMap(); + private Map parseProps(final JsonObject propsObj) { + if (propsObj == null) { + return Collections.emptyMap(); + } Map result = new HashMap<>(); propsObj.forEach((key, value) -> @@ -89,14 +111,6 @@ private Map parseProps(JsonObject propsObj) { return result; } - public void setEntryMap(Map entryMap) { - this.entryMap = entryMap; - } - - public void setElementSchema(JsonSchemaModel elementSchema) { - this.elementSchema = elementSchema; - } - public enum Type { RECORD(new Class[] { Record.class }), diff --git a/component-server-parent/component-server/pom.xml b/component-server-parent/component-server/pom.xml index 396c76c70503b..e66b7a97df095 100644 --- a/component-server-parent/component-server/pom.xml +++ b/component-server-parent/component-server/pom.xml @@ -190,7 +190,6 @@ org.talend.sdk.component component-server-api ${project.version} - test org.talend.sdk.component diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java index 93433cc4aa6fb..470c87fde404d 100644 --- a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java @@ -1,10 +1,23 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front; import java.util.List; -import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; import static org.talend.sdk.component.api.record.Schema.Type.LONG; -import static org.talend.sdk.component.api.record.Schema.Type.RECORD; import static org.talend.sdk.component.api.record.Schema.Type.STRING; import static org.junit.jupiter.api.Assertions.*; From f8878954822a0cc8e86fee322165c354963d7f0d Mon Sep 17 00:00:00 2001 From: yyin-talend Date: Thu, 25 Dec 2025 17:54:03 +0800 Subject: [PATCH 3/5] Add junit case --- .../server/front/model/JsonEntryModel.java | 9 +- .../server/front/JsonSchemaTest.java | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java index 6671932d5b1b7..61efd4ae0ce6b 100644 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java @@ -30,21 +30,18 @@ public class JsonEntryModel { private final JsonObject jsonEntry; - @Getter private final boolean isMetadata; /** * The name of this entry. */ @Setter - @Getter private String name; /** * The raw name of this entry. */ @Setter - @Getter private String rawName; /** @@ -58,7 +55,6 @@ public class JsonEntryModel { * Is this entry nullable or always valued. */ @Setter - @Getter private boolean nullable; /** @@ -77,7 +73,6 @@ public class JsonEntryModel { * Default value for this entry. */ @Setter - @Getter private Object defaultValue; /** @@ -91,11 +86,9 @@ public class JsonEntryModel { * Allows to associate to this field a comment - for doc purposes, no use in the runtime. */ @Setter - @Getter private String comment; @Setter - @Getter private boolean valid; /** @@ -143,7 +136,7 @@ public String getOriginalFieldName() { } public boolean isNullable() { - return jsonEntry.getBoolean("nullable", true); + return !jsonEntry.containsKey("nullable") || jsonEntry.getBoolean("nullable"); } public boolean isErrorCapable() { diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java index 470c87fde404d..1bb2c4e0bbb38 100644 --- a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java @@ -68,11 +68,121 @@ void shouldSerializeSchemaImplAndDeserializeToJsonSchemaModel() { assertEquals(2, model.getEntries().size()); assertEquals("id", model.getEntries().get(0).getName()); + assertEquals(JsonSchemaModel.Type.STRING, model.getEntries().get(0).getType()); + assertEquals("field2", model.getEntries().get(1).getName()); + assertEquals(JsonSchemaModel.Type.LONG, model.getEntries().get(1).getType()); + assertEquals("field2 comment", model.getEntries().get(1).getComment()); + assertEquals(false, model.getEntries().get(1).isNullable()); // entryMap built correctly assertTrue(model.getEntryMap().containsKey("id")); } + @Test + void testAllDataTypes() { + // given + final Schema schema = new SchemaImpl.BuilderImpl() // + .withType(Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("id") + .withType(Schema.Type.INT) + .withNullable(false) + .withErrorCapable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_date") + .withType(Schema.Type.DATETIME) + .withNullable(false) + .withErrorCapable(false) + .withComment("field date") + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_boolean") + .withType(Schema.Type.BOOLEAN) + .withNullable(true) + .withComment("field boolean") + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_bytes") + .withType(Schema.Type.BYTES) + .withComment("field bytes") + .withNullable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_decimal") + .withType(Schema.Type.DECIMAL) + .withComment("field decimal") + .withNullable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_double") + .withType(Schema.Type.DOUBLE) + .withComment("field double") + .withNullable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_float") + .withType(Schema.Type.FLOAT) + .withComment("field float") + .withNullable(true) + .build()) + .withProp("namespace", "test") + .build(); + + // when: serialize SchemaImpl + String json = jsonb.toJson(schema); + + // then: sanity check JSON + assertTrue(json.contains("\"type\":\"RECORD\"")); + assertTrue(json.contains("\"entries\"")); + + // when: deserialize into JsonSchemaModel + JsonSchemaModel model = new JsonSchemaModel(json); + + // then + assertEquals(JsonSchemaModel.Type.RECORD, model.getType()); + assertEquals("test", model.getProps().get("namespace")); + + assertEquals(7, model.getEntries().size()); + assertEquals("id", model.getEntries().get(0).getName()); + assertEquals(JsonSchemaModel.Type.INT, model.getEntries().get(0).getType()); + assertFalse(model.getEntries().get(0).isNullable()); + assertTrue(model.getEntries().get(0).isErrorCapable()); + assertEquals("field_date", model.getEntries().get(1).getName()); + assertEquals(JsonSchemaModel.Type.DATETIME, model.getEntries().get(1).getType()); + assertEquals("field date", model.getEntries().get(1).getComment()); + assertFalse(model.getEntries().get(1).isNullable()); + assertFalse(model.getEntries().get(1).isErrorCapable()); + + assertEquals("field_boolean", model.getEntries().get(2).getName()); + assertEquals(JsonSchemaModel.Type.BOOLEAN, model.getEntries().get(2).getType()); + assertEquals("field boolean", model.getEntries().get(2).getComment()); + assertTrue(model.getEntries().get(2).isNullable()); + assertFalse(model.getEntries().get(2).isErrorCapable()); + assertEquals("field_bytes", model.getEntries().get(3).getName()); + assertEquals(JsonSchemaModel.Type.BYTES, model.getEntries().get(3).getType()); + assertEquals("field bytes", model.getEntries().get(3).getComment()); + assertTrue(model.getEntries().get(3).isNullable()); + + assertEquals("field_decimal", model.getEntries().get(4).getName()); + assertEquals(JsonSchemaModel.Type.DECIMAL, model.getEntries().get(4).getType()); + assertEquals("field decimal", model.getEntries().get(4).getComment()); + assertTrue(model.getEntries().get(4).isNullable()); + assertEquals("field_double", model.getEntries().get(5).getName()); + assertEquals(JsonSchemaModel.Type.DOUBLE, model.getEntries().get(5).getType()); + assertEquals("field double", model.getEntries().get(5).getComment()); + assertTrue(model.getEntries().get(5).isNullable()); + + assertEquals("field_float", model.getEntries().get(6).getName()); + assertEquals(JsonSchemaModel.Type.FLOAT, model.getEntries().get(6).getType()); + assertEquals("field float", model.getEntries().get(6).getComment()); + assertTrue(model.getEntries().get(6).isNullable()); + // entryMap built correctly + assertTrue(model.getEntryMap().containsKey("id")); + assertEquals("id,field_date,field_boolean,field_bytes,field_decimal,field_double,field_float", + model.getProps().get("talend.fields.order")); + } + @Test void shouldParseArrayEntryElementSchema() { Schema.Entry nameEntry = new SchemaImpl.EntryImpl.BuilderImpl() From 46bd8162ee0c18420987b091723e58e3aa8a3642 Mon Sep 17 00:00:00 2001 From: yyin-talend Date: Mon, 5 Jan 2026 15:43:07 +0800 Subject: [PATCH 4/5] fix sonar issue --- .../server/front/model/JsonEntryModel.java | 39 +------------------ .../server/front/model/JsonSchemaModel.java | 9 ++--- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java index 61efd4ae0ce6b..a5302e565df3e 100644 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java @@ -30,20 +30,9 @@ public class JsonEntryModel { private final JsonObject jsonEntry; + @Getter private final boolean isMetadata; - /** - * The name of this entry. - */ - @Setter - private String name; - - /** - * The raw name of this entry. - */ - @Setter - private String rawName; - /** * Type of the entry, this determine which other fields are populated. */ @@ -51,17 +40,6 @@ public class JsonEntryModel { @Getter private JsonSchemaModel.Type type; - /** - * Is this entry nullable or always valued. - */ - @Setter - private boolean nullable; - - /** - * Is this entry can be in error. - */ - private boolean errorCapable; - /** * Is this entry a metadata entry. */ @@ -69,12 +47,6 @@ public class JsonEntryModel { @Getter private boolean metadata; - /** - * Default value for this entry. - */ - @Setter - private Object defaultValue; - /** * For type == record, the element type. */ @@ -82,15 +54,6 @@ public class JsonEntryModel { @Setter private JsonSchemaModel elementSchema; - /** - * Allows to associate to this field a comment - for doc purposes, no use in the runtime. - */ - @Setter - private String comment; - - @Setter - private boolean valid; - /** * metadata */ diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java index 5a083b6a34747..426321f2ff046 100644 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java @@ -51,15 +51,14 @@ public class JsonSchemaModel { @Getter @Setter - private List entries = new ArrayList(); + private List entries = new ArrayList<>(); @Getter - private List metadataEntries = new ArrayList(); - ; + private List metadataEntries = new ArrayList<>(); @Getter @Setter - private Map props = new HashMap(); + private Map props = new HashMap<>(); @Getter @Setter @@ -95,7 +94,7 @@ private List parseEntries(final JsonArray jsonArray, final boole return jsonArray.stream() .map(JsonValue::asJsonObject) .map(obj -> new JsonEntryModel(obj, isMetadata)) - .collect(Collectors.toList()); + .toList(); } private Map parseProps(final JsonObject propsObj) { From d4bf4b90b86adb488c017f1c528a46bd49670c99 Mon Sep 17 00:00:00 2001 From: ypiel Date: Fri, 16 Jan 2026 03:26:02 +0100 Subject: [PATCH 5/5] fix(QTDI-2215): Add schema/Entry pojo. (#1146) * fix(QTDI-2215): Add schema/Entry pojo. --------- Co-authored-by: yyin-talend --- .../component-server-model/pom.xml | 2 +- .../component/server/front/model/Entry.java | 107 ++ .../server/front/model/JsonEntryModel.java | 127 --- .../server/front/model/JsonSchemaModel.java | 155 --- .../component/server/front/model/Schema.java | 101 ++ .../component-server/pom.xml | 9 +- .../sdk/component/server/front/EntryTest.java | 146 +++ .../server/front/JsonSchemaTest.java | 357 ------- .../component/server/front/SchemaTest.java | 946 ++++++++++++++++++ 9 files changed, 1308 insertions(+), 642 deletions(-) create mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Entry.java delete mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java delete mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java create mode 100644 component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Schema.java create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/EntryTest.java delete mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/SchemaTest.java diff --git a/component-server-parent/component-server-model/pom.xml b/component-server-parent/component-server-model/pom.xml index 030254eeac938..7842a0230affc 100644 --- a/component-server-parent/component-server-model/pom.xml +++ b/component-server-parent/component-server-model/pom.xml @@ -76,4 +76,4 @@ - + \ No newline at end of file diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Entry.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Entry.java new file mode 100644 index 0000000000000..0839b41acead1 --- /dev/null +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Entry.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front.model; + +import java.beans.ConstructorProperties; +import java.util.LinkedHashMap; +import java.util.Map; + +import lombok.Data; + +@Data +public final class Entry { + + private final String name; + + private final String rawName; + + private final Schema.Type type; + + private final boolean nullable; + + private final boolean metadata; + + private final boolean errorCapable; + + private final boolean valid; + + private final Schema elementSchema; + + private final String comment; + + private final Map props = new LinkedHashMap<>(0); + + private final Object defaultValue; + + @ConstructorProperties({ "name", "rawName", "type", "nullable", "metadata", "errorCapable", + "valid", "elementSchema", "comment", "props", "defaultValue" }) + // Checkstyle off to let have 11 parameters to this constructor (normally 10 max) + // CHECKSTYLE:OFF + public Entry( + final String name, + final String rawName, + final Schema.Type type, + final boolean nullable, + final boolean metadata, + final boolean errorCapable, + final boolean valid, + final Schema elementSchema, + final String comment, + final Map props, + final Object defaultValue) { + // CHECKSTYLE:ON + this.name = name; + this.rawName = rawName; + this.type = type; + this.nullable = nullable; + this.metadata = metadata; + this.errorCapable = errorCapable; + this.valid = valid; + this.elementSchema = elementSchema; + this.comment = comment; + this.props.putAll(props); + this.defaultValue = defaultValue; + } + + private Object getInternalDefaultValue() { + return defaultValue; + } + + @SuppressWarnings("unchecked") + public T getDefaultValue() { + if (defaultValue == null) { + return null; + } + + return switch (this.getType()) { + case INT -> (T) ((Integer) ((Number) this.getInternalDefaultValue()).intValue()); + case LONG -> (T) ((Long) ((Number) this.getInternalDefaultValue()).longValue()); + case FLOAT -> (T) ((Float) ((Number) this.getInternalDefaultValue()).floatValue()); + case DOUBLE -> (T) ((Double) ((Number) this.getInternalDefaultValue()).doubleValue()); + default -> (T) this.getInternalDefaultValue(); + }; + + } + + public String getOriginalFieldName() { + return rawName != null ? rawName : name; + } + + public String getProp(final String key) { + return this.props.get(key); + } + +} \ No newline at end of file diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java deleted file mode 100644 index a5302e565df3e..0000000000000 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonEntryModel.java +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (C) 2006-2025 Talend Inc. - www.talend.com - * - * Licensed 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.talend.sdk.component.server.front.model; - -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.json.JsonObject; -import javax.json.JsonValue; - -import lombok.Getter; -import lombok.Setter; - -public class JsonEntryModel { - - private final JsonObject jsonEntry; - - @Getter - private final boolean isMetadata; - - /** - * Type of the entry, this determine which other fields are populated. - */ - @Setter - @Getter - private JsonSchemaModel.Type type; - - /** - * Is this entry a metadata entry. - */ - @Setter - @Getter - private boolean metadata; - - /** - * For type == record, the element type. - */ - @Getter - @Setter - private JsonSchemaModel elementSchema; - - /** - * metadata - */ - @Setter - @Getter - private Map props = new LinkedHashMap<>(0); - - JsonEntryModel(final JsonObject jsonEntry, final boolean isMetadata) { - this.jsonEntry = jsonEntry; - this.isMetadata = isMetadata; - this.type = JsonSchemaModel.Type.valueOf(jsonEntry.getString("type")); - this.props = parseProps(jsonEntry.getJsonObject("props")); - - // Parse element schema if present - this.elementSchema = jsonEntry.containsKey("elementSchema") - ? new JsonSchemaModel(jsonEntry.getJsonObject("elementSchema").toString()) - : null; - } - - private Map parseProps(final JsonObject propsObj) { - if (propsObj == null) { - return Collections.emptyMap(); - } - - Map result = new HashMap<>(); - propsObj.forEach((key, value) -> - result.put(key, value.getValueType() == JsonValue.ValueType.STRING - ? ((javax.json.JsonString) value).getString() - : value.toString())); - return result; - } - - public String getName() { - return jsonEntry.getString("name"); - } - - public String getRawName() { - return jsonEntry.getString("rawName", getName()); - } - - public String getOriginalFieldName() { - return getRawName() != null ? getRawName() : getName(); - } - - public boolean isNullable() { - return !jsonEntry.containsKey("nullable") || jsonEntry.getBoolean("nullable"); - } - - public boolean isErrorCapable() { - return jsonEntry.getBoolean("errorCapable", false); - } - - public boolean isValid() { - return jsonEntry.getBoolean("valid", true); - } - - public T getDefaultValue() { - return jsonEntry.containsKey("defaultValue") - ? (T) jsonEntry.get("defaultValue") - : null; - } - - public String getComment() { - return jsonEntry.getString("comment", null); - } - - public String getProp(final String property) { - return props.get(property); - } - -} diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java deleted file mode 100644 index 426321f2ff046..0000000000000 --- a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/JsonSchemaModel.java +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (C) 2006-2025 Talend Inc. - www.talend.com - * - * Licensed 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.talend.sdk.component.server.front.model; - -import java.io.StringReader; -import java.math.BigDecimal; -import java.time.temporal.Temporal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonValue; - -import lombok.Getter; -import lombok.Setter; - - -public class JsonSchemaModel { - - @Getter - @Setter - private Type type; - - private final JsonObject jsonSchema; - - @Setter - @Getter - private JsonSchemaModel elementSchema; - - @Getter - @Setter - private List entries = new ArrayList<>(); - - @Getter - private List metadataEntries = new ArrayList<>(); - - @Getter - @Setter - private Map props = new HashMap<>(); - - @Getter - @Setter - private Map entryMap = new HashMap<>(); - - public JsonSchemaModel(final String jsonString) { - try (JsonReader reader = Json.createReader(new StringReader(jsonString))) { - this.jsonSchema = reader.readObject(); - } - - this.type = Type.valueOf(jsonSchema.getString("type")); - this.props = parseProps(jsonSchema.getJsonObject("props")); - - // Parse entries - this.entries = parseEntries(jsonSchema.getJsonArray("entries"), false); - this.metadataEntries = parseEntries(jsonSchema.getJsonArray("metadata"), true); - - // Build entry map - this.entryMap = new HashMap<>(); - getEntries().forEach(e -> entryMap.put(e.getName(), e)); - - // Parse element schema for ARRAY types - this.elementSchema = jsonSchema.containsKey("elementSchema") - ? new JsonSchemaModel(jsonSchema.getJsonObject("elementSchema").toString()) - : null; - } - - private List parseEntries(final JsonArray jsonArray, final boolean isMetadata) { - if (jsonArray == null) { - return Collections.emptyList(); - } - - return jsonArray.stream() - .map(JsonValue::asJsonObject) - .map(obj -> new JsonEntryModel(obj, isMetadata)) - .toList(); - } - - private Map parseProps(final JsonObject propsObj) { - if (propsObj == null) { - return Collections.emptyMap(); - } - - Map result = new HashMap<>(); - propsObj.forEach((key, value) -> - result.put(key, value.getValueType() == JsonValue.ValueType.STRING - ? ((javax.json.JsonString) value).getString() - : value.toString())); - return result; - } - - public enum Type { - - RECORD(new Class[] { Record.class }), - ARRAY(new Class[] { Collection.class }), - STRING(new Class[] { String.class, Object.class }), - BYTES(new Class[] { byte[].class, Byte[].class }), - INT(new Class[] { Integer.class }), - LONG(new Class[] { Long.class }), - FLOAT(new Class[] { Float.class }), - DOUBLE(new Class[] { Double.class }), - BOOLEAN(new Class[] { Boolean.class }), - DATETIME(new Class[] { Long.class, Date.class, Temporal.class }), - DECIMAL(new Class[] { BigDecimal.class }); - - /** - * All compatibles Java classes - */ - private final Class[] classes; - - Type(final Class[] classes) { - this.classes = classes; - } - - /** - * Check if input can be affected to an entry of this type. - * - * @param input : object. - * - * @return true if input is null or ok. - */ - public boolean isCompatible(final Object input) { - if (input == null) { - return true; - } - for (final Class clazz : classes) { - if (clazz.isInstance(input)) { - return true; - } - } - return false; - } - } -} diff --git a/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Schema.java b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Schema.java new file mode 100644 index 0000000000000..50ef551ead82b --- /dev/null +++ b/component-server-parent/component-server-model/src/main/java/org/talend/sdk/component/server/front/model/Schema.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front.model; + +import java.beans.ConstructorProperties; +import java.math.BigDecimal; +import java.time.temporal.Temporal; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import lombok.Data; + +@Data +public final class Schema { + + private final Type type; + + private final Schema elementSchema; + + private final List entries; + + private final List metadata; + + private final Map props; + + @ConstructorProperties({ "type", "elementSchema", "entries", "metadata", "props" }) + public Schema( + final Type type, + final Schema elementSchema, + final List entries, + final List metadata, + final Map props) { + this.type = type; + this.elementSchema = elementSchema; + this.entries = entries; + this.metadata = metadata; + this.props = props; + } + + public String getProp(final String key) { + return this.props.get(key); + } + + public enum Type { + + RECORD(new Class[] { Record.class }), + ARRAY(new Class[] { Collection.class }), + STRING(new Class[] { String.class, Object.class }), + BYTES(new Class[] { byte[].class, Byte[].class }), + INT(new Class[] { Integer.class }), + LONG(new Class[] { Long.class }), + FLOAT(new Class[] { Float.class }), + DOUBLE(new Class[] { Double.class }), + BOOLEAN(new Class[] { Boolean.class }), + DATETIME(new Class[] { Long.class, Date.class, Temporal.class }), + DECIMAL(new Class[] { BigDecimal.class }); + + /** + * All compatibles Java classes + */ + private final Class[] classes; + + Type(final Class[] classes) { + this.classes = classes; + } + + /** + * Check if input can be affected to an entry of this type. + * + * @param input : object. + * + * @return true if input is null or ok. + */ + public boolean isCompatible(final Object input) { + if (input == null) { + return true; + } + for (final Class clazz : classes) { + if (clazz.isInstance(input)) { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/component-server-parent/component-server/pom.xml b/component-server-parent/component-server/pom.xml index e66b7a97df095..6cecd547fd78a 100644 --- a/component-server-parent/component-server/pom.xml +++ b/component-server-parent/component-server/pom.xml @@ -197,7 +197,12 @@ ${project.version} test - + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + @@ -493,4 +498,4 @@ - + \ No newline at end of file diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/EntryTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/EntryTest.java new file mode 100644 index 0000000000000..4cf8538a39862 --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/EntryTest.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; +import org.talend.sdk.component.api.record.SchemaProperty; +import org.talend.sdk.component.api.record.SchemaProperty.LogicalType; +import org.talend.sdk.component.runtime.record.RecordBuilderFactoryImpl; +import org.talend.sdk.component.server.front.model.Entry; +import org.talend.sdk.component.server.front.model.Schema; + +/** + * Unit tests for {@link Entry}. + */ +class EntryTest { + + private Entry createValidEntry() { + Map props = new LinkedHashMap<>(0); + props.put("p1", "v1"); + return new Entry("name", "raw", Schema.Type.STRING, true, false, true, + true, null, "comment", props, "default"); + } + + private Entry createEmptyEntry() { + Map props = new LinkedHashMap<>(0); + props.put("p1", "v1"); + return new Entry("name", null, Schema.Type.STRING, true, false, true, + true, null, null, props, null); + } + + // ---------------------------------------------------------------------- + // Builder + // ---------------------------------------------------------------------- + + @Test + void builderCreatesValidEntry() { + Entry entry = createValidEntry(); + + assertEquals("name", entry.getName()); + assertEquals("raw", entry.getRawName()); + assertEquals("raw", entry.getOriginalFieldName()); + assertEquals(Schema.Type.STRING, entry.getType()); + assertTrue(entry.isNullable()); + assertFalse(entry.isMetadata()); + assertTrue(entry.isErrorCapable()); + assertTrue(entry.isValid()); + assertEquals("comment", entry.getComment()); + assertEquals("default", entry.getDefaultValue()); + assertEquals("v1", entry.getProps().get("p1")); + } + + // ---------------------------------------------------------------------- + // Accessors + // ---------------------------------------------------------------------- + + @Test + void getDefaultValueIsTyped() { + Entry entry = createValidEntry(); + + String value = entry.getDefaultValue(); + assertEquals("default", value); + } + + @Test + void getDefaultValueEmpty() { + Entry entry = createEmptyEntry(); + + String value = entry.getDefaultValue(); + assertNull(value); + } + + @Test + void getPropReturnsProperty() { + Entry entry = createValidEntry(); + assertEquals("v1", entry.getProp("p1")); + assertNull(entry.getProp("k1")); + } + + // ---------------------------------------------------------------------- + // JSON deserialization + // ---------------------------------------------------------------------- + + @Test + void deserializeEntryFromJson() throws Exception { + RecordBuilderFactoryImpl factory = new RecordBuilderFactoryImpl("test"); + org.talend.sdk.component.api.record.Schema.Entry entryImpl = factory.newEntryBuilder() + .withName("éèfield") + .withLogicalType(LogicalType.UUID) + .withNullable(false) + .withMetadata(false) + .withErrorCapable(false) + .withComment("test comment") + .withProps(Map.of("p1", "v1")) + .withDefaultValue("defaultValue") + .build(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(entryImpl); + + ObjectMapper mapper = new ObjectMapper(); + Entry entry = mapper.readValue(json, Entry.class); + + assertEquals("_field", entry.getName()); + assertEquals("éèfield", entry.getRawName()); + assertEquals("éèfield", entry.getOriginalFieldName()); + assertEquals(Schema.Type.STRING, entry.getType()); + assertEquals(LogicalType.UUID.key(), entry.getProp(SchemaProperty.LOGICAL_TYPE)); + + assertFalse(entry.isNullable()); + assertFalse(entry.isMetadata()); + assertFalse(entry.isErrorCapable()); + assertTrue(entry.isValid()); + + assertEquals("test comment", entry.getComment()); + assertEquals("v1", entry.getProps().get("p1")); + assertEquals("defaultValue", entry.getDefaultValue()); + } + } + +} \ No newline at end of file diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java deleted file mode 100644 index 1bb2c4e0bbb38..0000000000000 --- a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/JsonSchemaTest.java +++ /dev/null @@ -1,357 +0,0 @@ -/** - * Copyright (C) 2006-2025 Talend Inc. - www.talend.com - * - * Licensed 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.talend.sdk.component.server.front; - -import java.util.List; -import org.junit.jupiter.api.Assertions; -import static org.talend.sdk.component.api.record.Schema.Type.LONG; -import static org.talend.sdk.component.api.record.Schema.Type.STRING; -import static org.junit.jupiter.api.Assertions.*; - -import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; -import org.junit.jupiter.api.Test; - -import org.talend.sdk.component.api.record.Schema; -import org.talend.sdk.component.runtime.record.SchemaImpl; -import org.talend.sdk.component.server.front.model.JsonEntryModel; -import org.talend.sdk.component.server.front.model.JsonSchemaModel; - -class JsonSchemaTest { - - private final Jsonb jsonb = JsonbBuilder.create(); - - @Test - void shouldSerializeSchemaImplAndDeserializeToJsonSchemaModel() { - // given - final Schema schema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.RECORD) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("id") - .withType(STRING) - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field2") - .withType(LONG) - .withNullable(false) - .withComment("field2 comment") - .build()) - .withProp("namespace", "test") - .build(); - - // when: serialize SchemaImpl - String json = jsonb.toJson(schema); - - // then: sanity check JSON - assertTrue(json.contains("\"type\":\"RECORD\"")); - assertTrue(json.contains("\"entries\"")); - - // when: deserialize into JsonSchemaModel - JsonSchemaModel model = new JsonSchemaModel(json); - - // then - assertEquals(JsonSchemaModel.Type.RECORD, model.getType()); - assertEquals("test", model.getProps().get("namespace")); - - assertEquals(2, model.getEntries().size()); - assertEquals("id", model.getEntries().get(0).getName()); - assertEquals(JsonSchemaModel.Type.STRING, model.getEntries().get(0).getType()); - assertEquals("field2", model.getEntries().get(1).getName()); - assertEquals(JsonSchemaModel.Type.LONG, model.getEntries().get(1).getType()); - assertEquals("field2 comment", model.getEntries().get(1).getComment()); - assertEquals(false, model.getEntries().get(1).isNullable()); - - // entryMap built correctly - assertTrue(model.getEntryMap().containsKey("id")); - } - - @Test - void testAllDataTypes() { - // given - final Schema schema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.RECORD) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("id") - .withType(Schema.Type.INT) - .withNullable(false) - .withErrorCapable(true) - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_date") - .withType(Schema.Type.DATETIME) - .withNullable(false) - .withErrorCapable(false) - .withComment("field date") - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_boolean") - .withType(Schema.Type.BOOLEAN) - .withNullable(true) - .withComment("field boolean") - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_bytes") - .withType(Schema.Type.BYTES) - .withComment("field bytes") - .withNullable(true) - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_decimal") - .withType(Schema.Type.DECIMAL) - .withComment("field decimal") - .withNullable(true) - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_double") - .withType(Schema.Type.DOUBLE) - .withComment("field double") - .withNullable(true) - .build()) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field_float") - .withType(Schema.Type.FLOAT) - .withComment("field float") - .withNullable(true) - .build()) - .withProp("namespace", "test") - .build(); - - // when: serialize SchemaImpl - String json = jsonb.toJson(schema); - - // then: sanity check JSON - assertTrue(json.contains("\"type\":\"RECORD\"")); - assertTrue(json.contains("\"entries\"")); - - // when: deserialize into JsonSchemaModel - JsonSchemaModel model = new JsonSchemaModel(json); - - // then - assertEquals(JsonSchemaModel.Type.RECORD, model.getType()); - assertEquals("test", model.getProps().get("namespace")); - - assertEquals(7, model.getEntries().size()); - assertEquals("id", model.getEntries().get(0).getName()); - assertEquals(JsonSchemaModel.Type.INT, model.getEntries().get(0).getType()); - assertFalse(model.getEntries().get(0).isNullable()); - assertTrue(model.getEntries().get(0).isErrorCapable()); - assertEquals("field_date", model.getEntries().get(1).getName()); - assertEquals(JsonSchemaModel.Type.DATETIME, model.getEntries().get(1).getType()); - assertEquals("field date", model.getEntries().get(1).getComment()); - assertFalse(model.getEntries().get(1).isNullable()); - assertFalse(model.getEntries().get(1).isErrorCapable()); - - assertEquals("field_boolean", model.getEntries().get(2).getName()); - assertEquals(JsonSchemaModel.Type.BOOLEAN, model.getEntries().get(2).getType()); - assertEquals("field boolean", model.getEntries().get(2).getComment()); - assertTrue(model.getEntries().get(2).isNullable()); - assertFalse(model.getEntries().get(2).isErrorCapable()); - assertEquals("field_bytes", model.getEntries().get(3).getName()); - assertEquals(JsonSchemaModel.Type.BYTES, model.getEntries().get(3).getType()); - assertEquals("field bytes", model.getEntries().get(3).getComment()); - assertTrue(model.getEntries().get(3).isNullable()); - - assertEquals("field_decimal", model.getEntries().get(4).getName()); - assertEquals(JsonSchemaModel.Type.DECIMAL, model.getEntries().get(4).getType()); - assertEquals("field decimal", model.getEntries().get(4).getComment()); - assertTrue(model.getEntries().get(4).isNullable()); - assertEquals("field_double", model.getEntries().get(5).getName()); - assertEquals(JsonSchemaModel.Type.DOUBLE, model.getEntries().get(5).getType()); - assertEquals("field double", model.getEntries().get(5).getComment()); - assertTrue(model.getEntries().get(5).isNullable()); - - assertEquals("field_float", model.getEntries().get(6).getName()); - assertEquals(JsonSchemaModel.Type.FLOAT, model.getEntries().get(6).getType()); - assertEquals("field float", model.getEntries().get(6).getComment()); - assertTrue(model.getEntries().get(6).isNullable()); - // entryMap built correctly - assertTrue(model.getEntryMap().containsKey("id")); - assertEquals("id,field_date,field_boolean,field_bytes,field_decimal,field_double,field_float", - model.getProps().get("talend.fields.order")); - } - - @Test - void shouldParseArrayEntryElementSchema() { - Schema.Entry nameEntry = new SchemaImpl.EntryImpl.BuilderImpl() - .withName("name") - .withNullable(true) - .withType(Schema.Type.STRING) - .build(); - Schema.Entry ageEntry = new SchemaImpl.EntryImpl.BuilderImpl() - .withName("age") - .withNullable(true) - .withType(Schema.Type.INT) - .build(); - Schema customerSchema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.RECORD) - .withEntry(nameEntry) - .withEntry(ageEntry) - .build(); - - final Schema schema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.ARRAY) // - .withElementSchema(customerSchema) - .withProp("namespace", "test") - .build(); - - String json = jsonb.toJson(schema); - JsonSchemaModel model = new JsonSchemaModel(json); - - JsonSchemaModel innerSchema = model.getElementSchema(); - - assertEquals(JsonSchemaModel.Type.ARRAY, model.getType()); - assertNotNull(innerSchema); - assertEquals(JsonSchemaModel.Type.RECORD, innerSchema.getType()); - //check entry name - final List entryNames = innerSchema.getEntries().stream().map(JsonEntryModel::getName) - .toList(); - Assertions.assertEquals(2, entryNames.size()); - Assertions.assertTrue(entryNames.contains("name")); - Assertions.assertTrue(entryNames.contains("age")); - - //check entry type - final List entryTypes = innerSchema.getEntries().stream().map(JsonEntryModel::getType) - .toList(); - Assertions.assertTrue(entryTypes.contains(JsonSchemaModel.Type.INT)); - Assertions.assertTrue(entryTypes.contains(JsonSchemaModel.Type.STRING)); - } - - - @Test - void shouldMarkMetadataEntries() { - Schema.Entry meta1 = new SchemaImpl.EntryImpl.BuilderImpl() // - .withName("meta1") // - .withType(Schema.Type.INT) // - .withMetadata(true) // - .build(); - - Schema.Entry meta2 = new SchemaImpl.EntryImpl.BuilderImpl() // - .withName("meta2") // - .withType(Schema.Type.STRING) // - .withMetadata(true) // - .withNullable(true) // - .build(); - - Schema.Entry data1 = new SchemaImpl.EntryImpl.BuilderImpl() // - .withName("data1") // - .withType(Schema.Type.INT) // - .build(); - Schema.Entry data2 = new SchemaImpl.EntryImpl.BuilderImpl() // - .withName("data2") // - .withType(Schema.Type.STRING) // - .withNullable(true) // - .build(); - - Schema schema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.RECORD) // - .withEntry(data1) // - .withEntry(meta1) // - .withEntry(data2) // - .withEntry(meta2) // - .build(); - - String json = jsonb.toJson(schema); - JsonSchemaModel model = new JsonSchemaModel(json); - - JsonEntryModel metaEntry = model.getMetadataEntries().get(0); - - //check entry name - final List entryNames = model.getEntries().stream().map(JsonEntryModel::getName) - .toList(); - Assertions.assertEquals(2, entryNames.size()); - Assertions.assertTrue(entryNames.contains(data1.getName())); - Assertions.assertTrue(entryNames.contains(data2.getName())); - Assertions.assertEquals(4, schema.getAllEntries().count()); - - //check entry type - final List entryTypes = model.getEntries().stream().map(JsonEntryModel::getType) - .map(JsonSchemaModel.Type::name) - .toList(); - Assertions.assertEquals(2, entryTypes.size()); - Assertions.assertTrue(entryTypes.contains(data1.getType().name())); - Assertions.assertTrue(entryTypes.contains(data2.getType().name())); - - //check meta name - final List metaEntryNames = model.getMetadataEntries().stream().map(JsonEntryModel::getName) - .toList(); - Assertions.assertEquals(2, metaEntryNames.size()); - Assertions.assertTrue(metaEntryNames.contains(meta1.getName())); - Assertions.assertTrue(metaEntryNames.contains(meta2.getName())); - - //check meta type - final List metaEntryTypes = model.getMetadataEntries().stream().map(JsonEntryModel::getType) - .map(JsonSchemaModel.Type::name) - .toList(); - Assertions.assertEquals(2, metaEntryTypes.size()); - Assertions.assertTrue(metaEntryTypes.contains(meta1.getType().name())); - Assertions.assertTrue(metaEntryTypes.contains(meta2.getType().name())); - - assertTrue(metaEntry.isMetadata()); - assertEquals("meta1", metaEntry.getName()); - } - - @Test - void shouldParseEntryProps() { - Schema schema = new SchemaImpl.BuilderImpl() // - .withType(Schema.Type.RECORD) - //.type(Schema.Type.RECORD) - .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() - .withName("field") - .withType(Schema.Type.STRING) - .withProp("format", "email") - .build()) - .build(); - - String json = jsonb.toJson(schema); - JsonSchemaModel model = new JsonSchemaModel(json); - - JsonEntryModel entry = model.getEntries().get(0); - - assertEquals("email", entry.getProps().get("format")); - } - - @Test - void shouldFailForInvalidEntryType() { - String invalidJson = """ - { - "type": "RECORD", - "entries": [ - { "name": "x", "type": "INVALID" } - ] - } - """; - - assertThrows(IllegalArgumentException.class, - () -> new JsonSchemaModel(invalidJson)); - } - - - @Test - void shouldFailForInvalidSchemaType() { - String invalidJson = """ - { - "type": "NOT_VALID", - "entries": [] - } - """; - - assertThrows(IllegalArgumentException.class, - () -> new JsonSchemaModel(invalidJson)); - } - -} - - diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/SchemaTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/SchemaTest.java new file mode 100644 index 0000000000000..65eeea0feeab9 --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/front/SchemaTest.java @@ -0,0 +1,946 @@ +/** + * Copyright (C) 2006-2025 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.front; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.talend.sdk.component.api.record.Schema.Type.LONG; +import static org.talend.sdk.component.api.record.Schema.Type.STRING; + +import java.io.StringReader; +import java.util.List; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.talend.sdk.component.api.record.SchemaProperty; +import org.talend.sdk.component.api.record.SchemaProperty.LogicalType; +import org.talend.sdk.component.runtime.record.RecordBuilderFactoryImpl; +import org.talend.sdk.component.runtime.record.SchemaImpl; +import org.talend.sdk.component.server.front.model.Entry; +import org.talend.sdk.component.server.front.model.Schema; + +class SchemaTest { + + private final Jsonb jsonb = JsonbBuilder.create(); + + @Test + void shouldSerializeSchemaImplAndDeserializeToJsonSchemaModel() { + // given + final org.talend.sdk.component.api.record.Schema schema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("id") + .withType(STRING) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field2") + .withType(LONG) + .withNullable(false) + .withComment("field2 comment") + .build()) + .withProp("namespace", "test") + .build(); + + // when: serialize SchemaImpl + String json = jsonb.toJson(schema); + + // then: sanity check JSON + assertTrue(json.contains("\"type\":\"RECORD\"")); + assertTrue(json.contains("\"entries\"")); + + // when: deserialize into Schema + org.talend.sdk.component.server.front.model.Schema model = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + // then + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.RECORD, model.getType()); + assertEquals("test", model.getProps().get("namespace")); + + assertEquals(2, model.getEntries().size()); + assertEquals("id", model.getEntries().get(0).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.STRING, + model.getEntries().get(0).getType()); + assertEquals("field2", model.getEntries().get(1).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.LONG, model.getEntries().get(1).getType()); + assertEquals("field2 comment", model.getEntries().get(1).getComment()); + assertFalse(model.getEntries().get(1).isNullable()); + + } + + @Test + void deserializeRecordSchemaWithEntriesAndMetadata() { + String json = + """ + { + "type": "RECORD", + "props": { + "p1": "v1" + }, + "entries": [ + { + "name": "id", + "rawName": "id", + "originalFieldName": "id", + "type": "INT", + "nullable": false, + "metadata": false, + "errorCapable": false, + "valid": true, + "defaultValue": 0, + "comment": "identifier", + "props": { + "logicalType": "int" + } + } + ], + "metadata": [ + { + "name": "source", + "rawName": "source", + "originalFieldName": "source", + "type": "STRING", + "nullable": true, + "metadata": true, + "errorCapable": false, + "valid": true, + "comment": "meta field", + "props": { + "m": "v" + } + } + ] + } + """; + + org.talend.sdk.component.server.front.model.Schema schema = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + assertNotNull(schema); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.RECORD, schema.getType()); + + // ---- props + assertEquals("v1", schema.getProp("p1")); + assertEquals("v1", schema.getProps().get("p1")); + + // ---- entries + List entries = schema.getEntries(); + assertEquals(1, entries.size()); + + Entry id = entries.get(0); + assertEquals("id", id.getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.INT, id.getType()); + assertFalse(id.isNullable()); + assertEquals("identifier", id.getComment()); + assertEquals("int", id.getProp("logicalType")); + + // ---- metadata + List metadata = schema.getMetadata(); + assertEquals(1, metadata.size()); + + Entry source = metadata.get(0); + assertTrue(source.isMetadata()); + assertEquals("source", source.getName()); + assertEquals("v", source.getProp("m")); + + // ---- entry lookup + assertSame(id, schema.getEntries().get(0)); + assertSame(source, schema.getMetadata().get(0)); + } + + @Test + void deserializeArraySchemaWithElementSchema() { + String json = + """ + { + "type": "ARRAY", + "elementSchema": { + "type": "STRING" + } + } + """; + + org.talend.sdk.component.server.front.model.Schema schema = jsonb.fromJson(json, + org.talend.sdk.component.server.front.model.Schema.class); + + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.ARRAY, schema.getType()); + assertNotNull(schema.getElementSchema()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.STRING, + schema.getElementSchema().getType()); + } + + @Test + void jsonPropIsParsedAsJsonWhenPossible() { + String json = + """ + { + "type": "RECORD", + "props": { + "config": "{\\"a\\":1}" + } + } + """; + + org.talend.sdk.component.server.front.model.Schema schema = jsonb.fromJson(json, + org.talend.sdk.component.server.front.model.Schema.class); + + assertNotNull(schema.getProp("config")); + assertEquals("{\"a\":1}", schema.getProp("config")); + } + + @Test + void unknownPropertyIsNull() { + String json = + """ + { + "type": "RECORD" + } + """; + + org.talend.sdk.component.server.front.model.Schema schema = jsonb.fromJson(json, + org.talend.sdk.component.server.front.model.Schema.class); + + assertNull(schema.getProps()); + } + + @Test + void testAllDataTypes1() { + // given + final org.talend.sdk.component.api.record.Schema schema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("id") + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) + .withNullable(false) + .withErrorCapable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_date") + .withType(org.talend.sdk.component.api.record.Schema.Type.DATETIME) + .withNullable(false) + .withErrorCapable(false) + .withComment("field date") + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_boolean") + .withType(org.talend.sdk.component.api.record.Schema.Type.BOOLEAN) + .withNullable(true) + .withComment("field boolean") + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_bytes") + .withType(org.talend.sdk.component.api.record.Schema.Type.BYTES) + .withComment("field bytes") + .withNullable(true) + .build()) + .withProp("namespace", "test") + .build(); + + // when: serialize SchemaImpl + String json = jsonb.toJson(schema); + + // then: sanity check JSON + assertTrue(json.contains("\"type\":\"RECORD\"")); + assertTrue(json.contains("\"entries\"")); + + // when: deserialize into Schema + org.talend.sdk.component.server.front.model.Schema model = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + // then + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.RECORD, model.getType()); + assertEquals("test", model.getProps().get("namespace")); + + assertEquals(4, model.getEntries().size()); + assertEquals("id", model.getEntries().get(0).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.INT, model.getEntries().get(0).getType()); + assertFalse(model.getEntries().get(0).isNullable()); + assertTrue(model.getEntries().get(0).isErrorCapable()); + assertEquals("field_date", model.getEntries().get(1).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.DATETIME, + model.getEntries().get(1).getType()); + assertEquals("field date", model.getEntries().get(1).getComment()); + assertFalse(model.getEntries().get(1).isNullable()); + assertFalse(model.getEntries().get(1).isErrorCapable()); + + assertEquals("field_boolean", model.getEntries().get(2).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.BOOLEAN, + model.getEntries().get(2).getType()); + assertEquals("field boolean", model.getEntries().get(2).getComment()); + assertTrue(model.getEntries().get(2).isNullable()); + assertFalse(model.getEntries().get(2).isErrorCapable()); + assertEquals("field_bytes", model.getEntries().get(3).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.BYTES, + model.getEntries().get(3).getType()); + assertEquals("field bytes", model.getEntries().get(3).getComment()); + assertTrue(model.getEntries().get(3).isNullable()); + + assertEquals("id,field_date,field_boolean,field_bytes", + model.getProps().get("talend.fields.order")); + } + + @Test + void testAllDataTypes2() { + // given + final org.talend.sdk.component.api.record.Schema schema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_decimal") + .withType(org.talend.sdk.component.api.record.Schema.Type.DECIMAL) + .withComment("field decimal") + .withNullable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_double") + .withType(org.talend.sdk.component.api.record.Schema.Type.DOUBLE) + .withComment("field double") + .withNullable(true) + .build()) + .withEntry(new SchemaImpl.EntryImpl.BuilderImpl() + .withName("field_float") + .withType(org.talend.sdk.component.api.record.Schema.Type.FLOAT) + .withComment("field float") + .withNullable(true) + .build()) + .withProp("namespace", "test") + .build(); + + // when: serialize SchemaImpl + String json = jsonb.toJson(schema); + + // then: sanity check JSON + assertTrue(json.contains("\"type\":\"RECORD\"")); + assertTrue(json.contains("\"entries\"")); + + // when: deserialize into Schema + org.talend.sdk.component.server.front.model.Schema model = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + // then + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.RECORD, model.getType()); + assertEquals("test", model.getProps().get("namespace")); + + assertEquals(3, model.getEntries().size()); + + assertEquals("field_decimal", model.getEntries().get(0).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.DECIMAL, + model.getEntries().get(0).getType()); + assertEquals("field decimal", model.getEntries().get(0).getComment()); + assertTrue(model.getEntries().get(0).isNullable()); + assertEquals("field_double", model.getEntries().get(1).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.DOUBLE, + model.getEntries().get(1).getType()); + assertEquals("field double", model.getEntries().get(1).getComment()); + assertTrue(model.getEntries().get(1).isNullable()); + + assertEquals("field_float", model.getEntries().get(2).getName()); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.FLOAT, + model.getEntries().get(2).getType()); + assertEquals("field float", model.getEntries().get(2).getComment()); + assertTrue(model.getEntries().get(2).isNullable()); + + assertEquals("field_decimal,field_double,field_float", + model.getProps().get("talend.fields.order")); + } + + @Test + void shouldParseArrayEntryElementSchema() { + org.talend.sdk.component.api.record.Schema.Entry nameEntry = new SchemaImpl.EntryImpl.BuilderImpl() + .withName("name") + .withNullable(true) + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) + .build(); + org.talend.sdk.component.api.record.Schema.Entry ageEntry = new SchemaImpl.EntryImpl.BuilderImpl() + .withName("age") + .withNullable(true) + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) + .build(); + org.talend.sdk.component.api.record.Schema customerSchema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withEntry(nameEntry) + .withEntry(ageEntry) + .build(); + + final org.talend.sdk.component.api.record.Schema schema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.ARRAY) // + .withElementSchema(customerSchema) + .withProp("namespace", "test") + .build(); + + String json = jsonb.toJson(schema); + // when: deserialize into Schema + org.talend.sdk.component.server.front.model.Schema model = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + org.talend.sdk.component.server.front.model.Schema innerSchema = model.getElementSchema(); + + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.ARRAY, model.getType()); + assertNotNull(innerSchema); + assertEquals(org.talend.sdk.component.server.front.model.Schema.Type.RECORD, innerSchema.getType()); + // check entry name + final List entryNames = innerSchema.getEntries() + .stream() + .map(Entry::getName) + .toList(); + Assertions.assertEquals(2, entryNames.size()); + Assertions.assertTrue(entryNames.contains("name")); + Assertions.assertTrue(entryNames.contains("age")); + + // check entry type + final List entryTypes = + innerSchema.getEntries().stream().map(Entry::getType).toList(); + Assertions.assertTrue(entryTypes.contains(org.talend.sdk.component.server.front.model.Schema.Type.INT)); + Assertions.assertTrue(entryTypes.contains(org.talend.sdk.component.server.front.model.Schema.Type.STRING)); + } + + @Test + void shouldMarkMetadataEntries() { + org.talend.sdk.component.api.record.Schema.Entry meta1 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("meta1") // + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) // + .withMetadata(true) // + .build(); + + org.talend.sdk.component.api.record.Schema.Entry meta2 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("meta2") // + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) // + .withMetadata(true) // + .withNullable(true) // + .build(); + + org.talend.sdk.component.api.record.Schema.Entry data1 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("data1") // + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) // + .build(); + org.talend.sdk.component.api.record.Schema.Entry data2 = new SchemaImpl.EntryImpl.BuilderImpl() // + .withName("data2") // + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) // + .withNullable(true) // + .build(); + + org.talend.sdk.component.api.record.Schema schema = new SchemaImpl.BuilderImpl() // + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) // + .withEntry(data1) // + .withEntry(meta1) // + .withEntry(data2) // + .withEntry(meta2) // + .build(); + + String json = jsonb.toJson(schema); + // when: deserialize into Schema + org.talend.sdk.component.server.front.model.Schema model = jsonb.fromJson(new StringReader(json), + org.talend.sdk.component.server.front.model.Schema.class); + + org.talend.sdk.component.server.front.model.Entry metaEntry = model.getMetadata().get(0); + + // check entry name + final List entryNames = model.getEntries() + .stream() + .map(Entry::getName) + .toList(); + Assertions.assertEquals(2, entryNames.size()); + Assertions.assertTrue(entryNames.contains(data1.getName())); + Assertions.assertTrue(entryNames.contains(data2.getName())); + Assertions.assertEquals(4, schema.getAllEntries().count()); + + // check entry type + final List entryTypes = model.getEntries() + .stream() + .map(Entry::getType) + .map(org.talend.sdk.component.server.front.model.Schema.Type::name) + .toList(); + Assertions.assertEquals(2, entryTypes.size()); + Assertions.assertTrue(entryTypes.contains(data1.getType().name())); + Assertions.assertTrue(entryTypes.contains(data2.getType().name())); + + // check meta name + final List metaEntryNames = model.getMetadata() + .stream() + .map(Entry::getName) + .toList(); + Assertions.assertEquals(2, metaEntryNames.size()); + Assertions.assertTrue(metaEntryNames.contains(meta1.getName())); + Assertions.assertTrue(metaEntryNames.contains(meta2.getName())); + + // check meta type + final List metaEntryTypes = model.getMetadata() + .stream() + .map(Entry::getType) + .map(org.talend.sdk.component.server.front.model.Schema.Type::name) + .toList(); + Assertions.assertEquals(2, metaEntryTypes.size()); + Assertions.assertTrue(metaEntryTypes.contains(meta1.getType().name())); + Assertions.assertTrue(metaEntryTypes.contains(meta2.getType().name())); + + assertTrue(metaEntry.isMetadata()); + assertEquals("meta1", metaEntry.getName()); + } + + @Test + void deserializeCompleteSchemaWithAllTypesAndParameters() throws Exception { + RecordBuilderFactoryImpl factory = new RecordBuilderFactoryImpl("test"); + + // Build a comprehensive schema with all supported types and all field parameters + org.talend.sdk.component.api.record.Schema schemaImpl = + factory.newSchemaBuilder(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withProp("namespace", "com.talend.test") + .withProp("doc", "Comprehensive schema test") + .withProp("customProp1", "value1") + .withProp("customProp2", "value2") + // STRING type with various parameters + .withEntry(factory.newEntryBuilder() + .withName("stringField") + .withRawName("string_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) + .withNullable(false) + .withDefaultValue("default string") + .withComment("String field with default value") + .withProp(SchemaProperty.SIZE, "255") + .withProp(SchemaProperty.PATTERN, "[a-zA-Z0-9]+") + .withProp(SchemaProperty.IS_KEY, "true") + .build()) + // INT type + .withEntry(factory.newEntryBuilder() + .withName("intField") + .withRawName("int_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) + .withNullable(true) + .withDefaultValue(42) + .withComment("Integer field with default") + .withProp(SchemaProperty.ORIGIN_TYPE, "integer") + .build()) + // LONG type with error handling + .withEntry(factory.newEntryBuilder() + .withName("longField") + .withRawName("long_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.LONG) + .withNullable(false) + .withErrorCapable(true) + .withComment("Long field with error capability") + .withProp(SchemaProperty.IS_UNIQUE, "true") + .build()) + // FLOAT type + .withEntry(factory.newEntryBuilder() + .withName("floatField") + .withRawName("float_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.FLOAT) + .withNullable(true) + .withDefaultValue(3.14f) + .withComment("Float field") + .withProp(SchemaProperty.SCALE, "2") + .build()) + // DOUBLE type + .withEntry(factory.newEntryBuilder() + .withName("doubleField") + .withRawName("double_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.DOUBLE) + .withNullable(false) + .withDefaultValue(2.718281828) + .withComment("Double field with high precision") + .withProp(SchemaProperty.SCALE, "9") + .build()) + // BOOLEAN type + .withEntry(factory.newEntryBuilder() + .withName("booleanField") + .withRawName("boolean_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.BOOLEAN) + .withNullable(true) + .withDefaultValue(true) + .withComment("Boolean field") + .build()) + // BYTES type + .withEntry(factory.newEntryBuilder() + .withName("bytesField") + .withRawName("bytes_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.BYTES) + .withNullable(true) + .withComment("Bytes field for binary data") + .withProp(SchemaProperty.SIZE, "1024") + .build()) + // DECIMAL type + .withEntry(factory.newEntryBuilder() + .withName("decimalField") + .withRawName("decimal_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.DECIMAL) + .withNullable(false) + .withComment("Decimal field for precise calculations") + .withProp(SchemaProperty.SIZE, "10") + .withProp(SchemaProperty.SCALE, "2") + .build()) + // DATETIME type with DATE logical type + .withEntry(factory.newEntryBuilder() + .withName("dateField") + .withRawName("date_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.DATETIME) + .withLogicalType(LogicalType.DATE) + .withNullable(true) + .withComment("Date field with DATE logical type") + .withProp(SchemaProperty.PATTERN, "yyyy-MM-dd") + .build()) + // DATETIME type with TIME logical type + .withEntry(factory.newEntryBuilder() + .withName("timeField") + .withRawName("time_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.DATETIME) + .withLogicalType(LogicalType.TIME) + .withNullable(true) + .withComment("Time field with TIME logical type") + .withProp(SchemaProperty.PATTERN, "HH:mm:ss") + .build()) + // DATETIME type with TIMESTAMP logical type + .withEntry(factory.newEntryBuilder() + .withName("timestampField") + .withRawName("timestamp_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.DATETIME) + .withLogicalType(LogicalType.TIMESTAMP) + .withNullable(false) + .withComment("Timestamp field") + .withProp(SchemaProperty.PATTERN, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .build()) + // STRING type with UUID logical type + .withEntry(factory.newEntryBuilder() + .withName("uuidField") + .withRawName("uuid_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) + .withLogicalType(LogicalType.UUID) + .withNullable(true) + .withComment("UUID field") + .withProp(SchemaProperty.IS_FOREIGN_KEY, "true") + .build()) + // ARRAY type with primitive element schema + .withEntry(factory.newEntryBuilder() + .withName("stringArrayField") + .withRawName("string_array_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.ARRAY) + .withElementSchema( + factory.newSchemaBuilder(org.talend.sdk.component.api.record.Schema.Type.STRING) + .build()) + .withNullable(true) + .withComment("Array of strings") + .build()) + // ARRAY type with complex element schema + .withEntry(factory.newEntryBuilder() + .withName("intArrayField") + .withRawName("int_array_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.ARRAY) + .withElementSchema( + factory.newSchemaBuilder(org.talend.sdk.component.api.record.Schema.Type.INT) + .build()) + .withNullable(false) + .withComment("Array of integers") + .build()) + // RECORD type (nested record) + .withEntry(factory.newEntryBuilder() + .withName("nestedRecord") + .withRawName("nested_record") + .withType(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withElementSchema( + factory.newSchemaBuilder(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withProp("nestedNamespace", "com.talend.nested") + .withEntry(factory.newEntryBuilder() + .withName("nestedString") + .withRawName("nested_string") + .withType( + org.talend.sdk.component.api.record.Schema.Type.STRING) + .withNullable(true) + .withComment("Nested string field") + .build()) + .withEntry(factory.newEntryBuilder() + .withName("nestedInt") + .withRawName("nested_int") + .withType(org.talend.sdk.component.api.record.Schema.Type.INT) + .withNullable(false) + .withDefaultValue(100) + .withComment("Nested int field") + .build()) + .withEntry(factory.newEntryBuilder() + .withName("nestedDate") + .withRawName("nested_date") + .withType( + org.talend.sdk.component.api.record.Schema.Type.DATETIME) + .withLogicalType(LogicalType.DATE) + .withNullable(true) + .withComment("Nested date field") + .build()) + .build()) + .withNullable(true) + .withComment("Nested record structure") + .build()) + // ARRAY of RECORD type + .withEntry(factory.newEntryBuilder() + .withName("arrayOfRecordsField") + .withRawName("array_of_records_field") + .withType(org.talend.sdk.component.api.record.Schema.Type.ARRAY) + .withElementSchema( + factory.newSchemaBuilder(org.talend.sdk.component.api.record.Schema.Type.RECORD) + .withEntry(factory.newEntryBuilder() + .withName("recordInArrayString") + .withType( + org.talend.sdk.component.api.record.Schema.Type.STRING) + .withNullable(true) + .build()) + .withEntry(factory.newEntryBuilder() + .withName("recordInArrayLong") + .withType(org.talend.sdk.component.api.record.Schema.Type.LONG) + .withNullable(false) + .build()) + .build()) + .withNullable(true) + .withComment("Array of nested records") + .build()) + // Metadata entry + .withEntry(factory.newEntryBuilder() + .withName("metadataSource") + .withRawName("metadata_source") + .withType(org.talend.sdk.component.api.record.Schema.Type.STRING) + .withMetadata(true) + .withNullable(true) + .withComment("Metadata field indicating source") + .withProp("metaProp1", "metaValue1") + .build()) + .build(); + + // Serialize the schema to JSON + String json = jsonb.toJson(schemaImpl); + + // Deserialize using ObjectMapper (Jackson) + ObjectMapper mapper = new ObjectMapper(); + Schema schema = mapper.readValue(json, Schema.class); + + // ===== Validate top-level schema properties ===== + assertNotNull(schema); + assertEquals(Schema.Type.RECORD, schema.getType()); + assertNotNull(schema.getEntries()); + assertNotNull(schema.getProps()); + assertEquals("com.talend.test", schema.getProp("namespace")); + assertEquals("Comprehensive schema test", schema.getProp("doc")); + assertEquals("value1", schema.getProp("customProp1")); + assertEquals("value2", schema.getProp("customProp2")); + + // We have 17 data entries + 1 metadata entry = 18 total + // But entries list only contains data entries + assertEquals(schemaImpl.getEntries().size(), schema.getEntries().size()); + assertEquals(schemaImpl.getMetadata().size(), schema.getMetadata().size()); + + // ===== Validate STRING field ===== + Entry stringField = schema.getEntries().get(0); + assertEquals("stringField", stringField.getName()); + assertEquals("string_field", stringField.getRawName()); + assertEquals(Schema.Type.STRING, stringField.getType()); + assertFalse(stringField.isNullable()); + assertEquals("default string", stringField.getDefaultValue()); + assertEquals("String field with default value", stringField.getComment()); + assertEquals("255", stringField.getProp(SchemaProperty.SIZE)); + assertEquals("[a-zA-Z0-9]+", stringField.getProp(SchemaProperty.PATTERN)); + assertEquals("true", stringField.getProp(SchemaProperty.IS_KEY)); + assertFalse(stringField.isMetadata()); + + // ===== Validate INT field ===== + Entry intField = schema.getEntries().get(1); + assertEquals("intField", intField.getName()); + assertEquals("int_field", intField.getRawName()); + assertEquals(Schema.Type.INT, intField.getType()); + assertTrue(intField.isNullable()); + assertEquals(42, intField. getDefaultValue()); + assertEquals("Integer field with default", intField.getComment()); + assertEquals("integer", intField.getProp(SchemaProperty.ORIGIN_TYPE)); + + // ===== Validate LONG field with error capability ===== + Entry longField = schema.getEntries().get(2); + assertEquals("longField", longField.getName()); + assertEquals("long_field", longField.getRawName()); + assertEquals(Schema.Type.LONG, longField.getType()); + assertFalse(longField.isNullable()); + assertTrue(longField.isErrorCapable()); + assertEquals("Long field with error capability", longField.getComment()); + assertEquals("true", longField.getProp(SchemaProperty.IS_UNIQUE)); + + // ===== Validate FLOAT field ===== + Entry floatField = schema.getEntries().get(3); + assertEquals("floatField", floatField.getName()); + assertEquals("float_field", floatField.getRawName()); + assertEquals(Schema.Type.FLOAT, floatField.getType()); + assertTrue(floatField.isNullable()); + assertEquals(3.14f, floatField. getDefaultValue()); + assertEquals("Float field", floatField.getComment()); + assertEquals("2", floatField.getProp(SchemaProperty.SCALE)); + + // ===== Validate DOUBLE field ===== + Entry doubleField = schema.getEntries().get(4); + assertEquals("doubleField", doubleField.getName()); + assertEquals("double_field", doubleField.getRawName()); + assertEquals(Schema.Type.DOUBLE, doubleField.getType()); + assertFalse(doubleField.isNullable()); + assertEquals(2.718281828, doubleField.getDefaultValue()); + assertEquals("Double field with high precision", doubleField.getComment()); + assertEquals("9", doubleField.getProp(SchemaProperty.SCALE)); + + // ===== Validate BOOLEAN field ===== + Entry booleanField = schema.getEntries().get(5); + assertEquals("booleanField", booleanField.getName()); + assertEquals("boolean_field", booleanField.getRawName()); + assertEquals(Schema.Type.BOOLEAN, booleanField.getType()); + assertTrue(booleanField.isNullable()); + assertEquals(true, booleanField.getDefaultValue()); + assertEquals("Boolean field", booleanField.getComment()); + + // ===== Validate BYTES field ===== + Entry bytesField = schema.getEntries().get(6); + assertEquals("bytesField", bytesField.getName()); + assertEquals("bytes_field", bytesField.getRawName()); + assertEquals(Schema.Type.BYTES, bytesField.getType()); + assertTrue(bytesField.isNullable()); + assertEquals("Bytes field for binary data", bytesField.getComment()); + assertEquals("1024", bytesField.getProp(SchemaProperty.SIZE)); + + // ===== Validate DECIMAL field ===== + Entry decimalField = schema.getEntries().get(7); + assertEquals("decimalField", decimalField.getName()); + assertEquals("decimal_field", decimalField.getRawName()); + assertEquals(Schema.Type.DECIMAL, decimalField.getType()); + assertFalse(decimalField.isNullable()); + assertEquals("Decimal field for precise calculations", decimalField.getComment()); + assertEquals("10", decimalField.getProp(SchemaProperty.SIZE)); + assertEquals("2", decimalField.getProp(SchemaProperty.SCALE)); + + // ===== Validate DATE field (DATETIME with DATE logical type) ===== + Entry dateField = schema.getEntries().get(8); + assertEquals("dateField", dateField.getName()); + assertEquals("date_field", dateField.getRawName()); + assertEquals(Schema.Type.DATETIME, dateField.getType()); + assertEquals(LogicalType.DATE.key(), dateField.getProp(SchemaProperty.LOGICAL_TYPE)); + assertTrue(dateField.isNullable()); + assertEquals("Date field with DATE logical type", dateField.getComment()); + assertEquals("yyyy-MM-dd", dateField.getProp(SchemaProperty.PATTERN)); + + // ===== Validate TIME field (DATETIME with TIME logical type) ===== + Entry timeField = schema.getEntries().get(9); + assertEquals("timeField", timeField.getName()); + assertEquals("time_field", timeField.getRawName()); + assertEquals(Schema.Type.DATETIME, timeField.getType()); + assertEquals(LogicalType.TIME.key(), timeField.getProp(SchemaProperty.LOGICAL_TYPE)); + assertTrue(timeField.isNullable()); + assertEquals("Time field with TIME logical type", timeField.getComment()); + assertEquals("HH:mm:ss", timeField.getProp(SchemaProperty.PATTERN)); + + // ===== Validate TIMESTAMP field (DATETIME with TIMESTAMP logical type) ===== + Entry timestampField = schema.getEntries().get(10); + assertEquals("timestampField", timestampField.getName()); + assertEquals("timestamp_field", timestampField.getRawName()); + assertEquals(Schema.Type.DATETIME, timestampField.getType()); + assertEquals(LogicalType.TIMESTAMP.key(), timestampField.getProp(SchemaProperty.LOGICAL_TYPE)); + assertFalse(timestampField.isNullable()); + assertEquals("Timestamp field", timestampField.getComment()); + assertEquals("yyyy-MM-dd'T'HH:mm:ss.SSSZ", timestampField.getProp(SchemaProperty.PATTERN)); + + // ===== Validate UUID field (STRING with UUID logical type) ===== + Entry uuidField = schema.getEntries().get(11); + assertEquals("uuidField", uuidField.getName()); + assertEquals("uuid_field", uuidField.getRawName()); + assertEquals(Schema.Type.STRING, uuidField.getType()); + assertEquals(LogicalType.UUID.key(), uuidField.getProp(SchemaProperty.LOGICAL_TYPE)); + assertTrue(uuidField.isNullable()); + assertEquals("UUID field", uuidField.getComment()); + assertEquals("true", uuidField.getProp(SchemaProperty.IS_FOREIGN_KEY)); + + // ===== Validate ARRAY field with STRING elements ===== + Entry stringArrayField = schema.getEntries().get(12); + assertEquals("stringArrayField", stringArrayField.getName()); + assertEquals("string_array_field", stringArrayField.getRawName()); + assertEquals(Schema.Type.ARRAY, stringArrayField.getType()); + assertTrue(stringArrayField.isNullable()); + assertEquals("Array of strings", stringArrayField.getComment()); + assertNotNull(stringArrayField.getElementSchema()); + assertEquals(Schema.Type.STRING, stringArrayField.getElementSchema().getType()); + + // ===== Validate ARRAY field with INT elements ===== + Entry intArrayField = schema.getEntries().get(13); + assertEquals("intArrayField", intArrayField.getName()); + assertEquals("int_array_field", intArrayField.getRawName()); + assertEquals(Schema.Type.ARRAY, intArrayField.getType()); + assertFalse(intArrayField.isNullable()); + assertEquals("Array of integers", intArrayField.getComment()); + assertNotNull(intArrayField.getElementSchema()); + assertEquals(Schema.Type.INT, intArrayField.getElementSchema().getType()); + + // ===== Validate nested RECORD field ===== + Entry nestedRecord = schema.getEntries().get(14); + assertEquals("nestedRecord", nestedRecord.getName()); + assertEquals("nested_record", nestedRecord.getRawName()); + assertEquals(Schema.Type.RECORD, nestedRecord.getType()); + assertTrue(nestedRecord.isNullable()); + assertEquals("Nested record structure", nestedRecord.getComment()); + assertNotNull(nestedRecord.getElementSchema()); + assertEquals(Schema.Type.RECORD, nestedRecord.getElementSchema().getType()); + + // Validate nested record properties + Schema nestedSchema = nestedRecord.getElementSchema(); + assertEquals("com.talend.nested", nestedSchema.getProp("nestedNamespace")); + assertEquals(3, nestedSchema.getEntries().size()); + + // Validate nested string field + Entry nestedString = nestedSchema.getEntries().get(0); + assertEquals("nestedString", nestedString.getName()); + assertEquals("nested_string", nestedString.getRawName()); + assertEquals(Schema.Type.STRING, nestedString.getType()); + assertTrue(nestedString.isNullable()); + assertEquals("Nested string field", nestedString.getComment()); + + // Validate nested int field + Entry nestedInt = nestedSchema.getEntries().get(1); + assertEquals("nestedInt", nestedInt.getName()); + assertEquals("nested_int", nestedInt.getRawName()); + assertEquals(Schema.Type.INT, nestedInt.getType()); + assertFalse(nestedInt.isNullable()); + assertEquals(100, nestedInt. getDefaultValue()); + assertEquals("Nested int field", nestedInt.getComment()); + + // Validate nested date field + Entry nestedDate = nestedSchema.getEntries().get(2); + assertEquals("nestedDate", nestedDate.getName()); + assertEquals("nested_date", nestedDate.getRawName()); + assertEquals(Schema.Type.DATETIME, nestedDate.getType()); + assertEquals(LogicalType.DATE.key(), nestedDate.getProp(SchemaProperty.LOGICAL_TYPE)); + assertTrue(nestedDate.isNullable()); + assertEquals("Nested date field", nestedDate.getComment()); + + // ===== Validate ARRAY type with nested RECORD ===== + Entry arrayOfRecordsField = schema.getEntries().get(15); + assertEquals("arrayOfRecordsField", arrayOfRecordsField.getName()); + assertEquals(Schema.Type.ARRAY, arrayOfRecordsField.getType()); + assertNotNull(arrayOfRecordsField.getElementSchema()); + assertEquals(Schema.Type.RECORD, arrayOfRecordsField.getElementSchema().getType()); + + // ===== Validate metadata entry ===== + assertEquals(1, schema.getMetadata().size()); + Entry metadataEntry = schema.getMetadata().get(0); + assertEquals("metadataSource", metadataEntry.getName()); + assertEquals("metadata_source", metadataEntry.getRawName()); + assertEquals(Schema.Type.STRING, metadataEntry.getType()); + assertTrue(metadataEntry.isMetadata()); + assertTrue(metadataEntry.isNullable()); + assertEquals("Metadata field indicating source", metadataEntry.getComment()); + assertEquals("metaValue1", metadataEntry.getProp("metaProp1")); + } +} \ No newline at end of file