diff --git a/component-api/src/main/java/org/talend/sdk/component/api/record/Schema.java b/component-api/src/main/java/org/talend/sdk/component/api/record/Schema.java index 210d903cca1e4..2dbca03f27eb3 100644 --- a/component-api/src/main/java/org/talend/sdk/component/api/record/Schema.java +++ b/component-api/src/main/java/org/talend/sdk/component/api/record/Schema.java @@ -17,20 +17,14 @@ import java.io.StringReader; import java.math.BigDecimal; -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.StandardCharsets; import java.time.temporal.Temporal; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Function; @@ -494,53 +488,9 @@ default Schema build(Comparator order) { * * @return avro compatible name. */ + @Deprecated static String sanitizeConnectionName(final String name) { - if (SKIP_SANITIZE || name == null || name.isEmpty()) { - return name; - } - - char current = name.charAt(0); - final CharsetEncoder ascii = Charset.forName(StandardCharsets.US_ASCII.name()).newEncoder(); - final boolean skipFirstChar = ((!ascii.canEncode(current)) || (!Character.isLetter(current) && current != '_')) - && name.length() > 1 && (!Character.isDigit(name.charAt(1))); - - final StringBuilder sanitizedBuilder = new StringBuilder(); - - if (!skipFirstChar) { - if (((!Character.isLetter(current)) && current != '_') || (!ascii.canEncode(current))) { - sanitizedBuilder.append('_'); - } else { - sanitizedBuilder.append(current); - } - } - for (int i = 1; i < name.length(); i++) { - current = name.charAt(i); - if (!ascii.canEncode(current)) { - if (Character.isLowerCase(current) || Character.isUpperCase(current)) { - sanitizedBuilder.append('_'); - } else { - final byte[] encoded = - Base64.getEncoder().encode(name.substring(i, i + 1).getBytes(StandardCharsets.UTF_8)); - final String enc = new String(encoded); - if (sanitizedBuilder.length() == 0 && Character.isDigit(enc.charAt(0))) { - sanitizedBuilder.append('_'); - } - for (int iter = 0; iter < enc.length(); iter++) { - if (Character.isLetterOrDigit(enc.charAt(iter))) { - sanitizedBuilder.append(enc.charAt(iter)); - } else { - sanitizedBuilder.append('_'); - } - } - } - } else if (Character.isLetterOrDigit(current)) { - sanitizedBuilder.append(current); - } else { - sanitizedBuilder.append('_'); - } - - } - return sanitizedBuilder.toString(); + return SchemaCompanionUtil.sanitizeName(name); } @RequiredArgsConstructor @@ -679,39 +629,13 @@ public int compare(final Entry e1, final Entry e2) { } } + /** + * Use instead {@since SchemaCompanionUtil#avoidCollision(Schema.Entry, Function, BiConsumer)} + */ + @Deprecated static Schema.Entry avoidCollision(final Schema.Entry newEntry, final Function entryGetter, final BiConsumer replaceFunction) { - if (SKIP_SANITIZE) { - return newEntry; - } - final Optional collisionedEntry = Optional.ofNullable(entryGetter // - .apply(newEntry.getName())) // - .filter((final Entry field) -> !Objects.equals(field, newEntry)); - if (!collisionedEntry.isPresent()) { - // No collision, return new entry. - return newEntry; - } - final Entry matchedEntry = collisionedEntry.get(); - final boolean matchedToChange = matchedEntry.getRawName() != null && !(matchedEntry.getRawName().isEmpty()); - if (matchedToChange) { - // the rename has to be applied on entry already inside schema, so replace. - replaceFunction.accept(matchedEntry.getName(), newEntry); - } else if (newEntry.getRawName() == null || newEntry.getRawName().isEmpty()) { - // try to add exactly same raw, skip the add here. - return null; - } - final Entry fieldToChange = matchedToChange ? matchedEntry : newEntry; - int indexForAnticollision = 1; - final String baseName = Schema.sanitizeConnectionName(fieldToChange.getRawName()); // recalc primiti name. - - String newName = baseName + "_" + indexForAnticollision; - while (entryGetter.apply(newName) != null) { - indexForAnticollision++; - newName = baseName + "_" + indexForAnticollision; - } - final Entry newFieldToAdd = fieldToChange.toBuilder().withName(newName).build(); - - return newFieldToAdd; // matchedToChange ? newFieldToAdd : newEntry; + return SchemaCompanionUtil.avoidCollision(newEntry, entryGetter, replaceFunction); } } diff --git a/component-api/src/main/java/org/talend/sdk/component/api/record/SchemaCompanionUtil.java b/component-api/src/main/java/org/talend/sdk/component/api/record/SchemaCompanionUtil.java new file mode 100644 index 0000000000000..6563c2442821f --- /dev/null +++ b/component-api/src/main/java/org/talend/sdk/component/api/record/SchemaCompanionUtil.java @@ -0,0 +1,165 @@ +/** + * 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.api.record; + +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.talend.sdk.component.api.record.Schema.Entry; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SchemaCompanionUtil { + + /** + * Sanitize name to be avro compatible. + * + * @param name : original name. + * + * @return avro compatible name. + */ + public static String sanitizeName(final String name) { + if (Schema.SKIP_SANITIZE || name == null || name.isEmpty()) { + return name; + } + + final CharsetEncoder ascii = StandardCharsets.US_ASCII.newEncoder(); + final StringBuilder sanitizedBuilder = new StringBuilder(); + final char firstLetter = sanitizeFirstLetter(name, ascii); + if (firstLetter != (char) -1) { + sanitizedBuilder.append(firstLetter); + } + + for (int i = 1; i < name.length(); i++) { + char current = name.charAt(i); + if (ascii.canEncode(current)) { + sanitizedBuilder.append(Character.isLetterOrDigit(current) ? current : '_'); + } else { + if (Character.isLowerCase(current) || Character.isUpperCase(current)) { + sanitizedBuilder.append('_'); + } else { + final byte[] encoded = base64(name.substring(i, i + 1)); + final String enc = new String(encoded, StandardCharsets.UTF_8); + if (sanitizedBuilder.length() == 0 && Character.isDigit(enc.charAt(0))) { + sanitizedBuilder.append('_'); + } + + for (int iter = 0; iter < enc.length(); iter++) { + final char encodedCurrentChar = enc.charAt(iter); + final char sanitizedLetter = Character.isLetterOrDigit(encodedCurrentChar) + ? encodedCurrentChar + : '_'; + sanitizedBuilder.append(sanitizedLetter); + } + } + } + + } + return sanitizedBuilder.toString(); + } + + private static byte[] base64(final String value) { + return Base64.getEncoder().encode(value.getBytes(StandardCharsets.UTF_8)); + } + + private static char sanitizeFirstLetter(final String name, final CharsetEncoder ascii) { + char current = name.charAt(0); + final boolean skipFirstChar = !(ascii.canEncode(current) && validFirstLetter(current)) + && name.length() > 1 && !Character.isDigit(name.charAt(1)); + + // indicates that first letter is not valid, so it has to be skipped. + // and because the next letter is valid (or can be sanitized) we can use it as first letter. + if (skipFirstChar) { + return (char) -1; + } + + if (validFirstLetter(current) && ascii.canEncode(current)) { + return current; + } else { + return '_'; + } + } + + private static boolean validFirstLetter(final char value) { + return Character.isLetter(value) || value == '_'; + } + + /** + * May return a different entry with different name. + */ + public static Schema.Entry avoidCollision(final Schema.Entry newEntry, + final Function entryGetter, + final BiConsumer replaceFunction) { + if (Schema.SKIP_SANITIZE) { + return newEntry; + } + + final Entry alreadyExistedEntry = findCollidedEntry(newEntry, entryGetter); + if (alreadyExistedEntry == null) { + // No collision, return new entry. + return newEntry; + } + + final boolean matchedToChange = !isEmpty(alreadyExistedEntry.getRawName()); + if (matchedToChange) { + // the rename has to be applied on entry already inside schema, so replace. (dunno why) + // replace existed entry with a new name + final String newSanitizedName = newNotCollidedName(entryGetter, alreadyExistedEntry.getRawName()); + final Entry updatedExistedEntry = alreadyExistedEntry.toBuilder() + .withName(newSanitizedName) + .build(); + replaceFunction.accept(alreadyExistedEntry.getName(), updatedExistedEntry); + return newEntry; + } else if (isEmpty(newEntry.getRawName())) { + // try to add exactly same raw, skip the add here. + return null; + } else { + // raw name isn't empty, so we need to create a new entry with a new name (sanitized). + final String newSanitizedName = newNotCollidedName(entryGetter, newEntry.getRawName()); + return newEntry.toBuilder() + .withName(newSanitizedName) + .build(); + } + } + + private static Entry findCollidedEntry(final Entry newEntry, final Function entryGetter) { + return Optional.ofNullable(entryGetter.apply(newEntry.getName())) + .filter(retrievedEntry -> !Objects.equals(retrievedEntry, newEntry)) + .orElse(null); + } + + private static String newNotCollidedName(final Function entryGetter, final String rawName) { + final String baseName = sanitizeName(rawName); + int indexForAnticollision = 1; + String newName = baseName + "_" + indexForAnticollision; + while (entryGetter.apply(newName) != null) { + indexForAnticollision++; + newName = baseName + "_" + indexForAnticollision; + } + return newName; + } + + private static boolean isEmpty(final String value) { + return value == null || value.isEmpty(); + } +} diff --git a/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaCompanionUtilTest.java b/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaCompanionUtilTest.java new file mode 100644 index 0000000000000..26e5fb9c96153 --- /dev/null +++ b/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaCompanionUtilTest.java @@ -0,0 +1,212 @@ +/** + * 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.api.record; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.talend.sdk.component.api.record.Schema.Entry; +import org.talend.sdk.component.api.record.Schema.Type; +import org.talend.sdk.component.api.test.MockEntry; + +import lombok.RequiredArgsConstructor; + +class SchemaCompanionUtilTest { + + @Test + void sanitizationPatternBasedCheck() { + final Pattern checkPattern = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$"); + final String nonAscii1 = SchemaCompanionUtil.sanitizeName("30_39歳"); + Assertions.assertTrue(checkPattern.matcher(nonAscii1).matches(), "'" + nonAscii1 + "' don't match"); + + final String ch1 = SchemaCompanionUtil.sanitizeName("世帯数分布"); + final String ch2 = SchemaCompanionUtil.sanitizeName("抽出率調整"); + Assertions.assertTrue(checkPattern.matcher(ch1).matches(), "'" + ch1 + "' don't match"); + Assertions.assertTrue(checkPattern.matcher(ch2).matches(), "'" + ch2 + "' don't match"); + Assertions.assertNotEquals(ch1, ch2); + + final Random rnd = new Random(); + final byte[] array = new byte[20]; // length is bounded by 7 + for (int i = 0; i < 150; i++) { + rnd.nextBytes(array); + final String randomString = new String(array, StandardCharsets.UTF_8); + final String sanitize = SchemaCompanionUtil.sanitizeName(randomString); + Assertions.assertTrue(checkPattern.matcher(sanitize).matches(), "'" + sanitize + "' don't match"); + + final String sanitize2 = SchemaCompanionUtil.sanitizeName(sanitize); + Assertions.assertEquals(sanitize, sanitize2); + } + } + + @Test + void sanitizeNull() { + Assertions.assertNull(SchemaCompanionUtil.sanitizeName(null)); + } + + @MethodSource("sanitizeCasesSource") + @ParameterizedTest + void sanitizeCases(final String expected, final String rawName) { + Assertions.assertEquals(expected, SchemaCompanionUtil.sanitizeName(rawName)); + } + + public static Stream sanitizeCasesSource() { + return Stream.of( + Arguments.of("", ""), + Arguments.of("_", "$"), + Arguments.of("_", "1"), + Arguments.of("_", "é"), + Arguments.of("H", "éH"), + Arguments.of("_1", "é1"), + Arguments.of("H_lloWorld", "HélloWorld"), + Arguments.of("oid", "$oid"), + Arguments.of("Hello_World_", " Hello World "), + Arguments.of("_23HelloWorld", "123HelloWorld"), + + Arguments.of("Hello_World_", "Hello-World$"), + Arguments.of("_656", "5656"), + Arguments.of("_____", "Істина"), + // not very good test, because it depends on base64 encoding + // (but I wanted to check that part in coverage by this test) + Arguments.of("_5q2z", "9歳")); + } + + @Test + void noCollisionDuplicatedEntry() { + final String name = "name_b"; + + final Map entries = new HashMap<>(); + addNewEntry(newEntry(name, Type.STRING), entries); + addNewEntry(newEntry(name, Type.STRING), entries); + + // second entry with the same name was ignored (can't be two same raw names) + Assertions.assertEquals(1, entries.size()); + + Assertions.assertNull(entries.get(name).getRawName()); + Assertions.assertEquals(name, entries.get("name_b").getName()); + } + + @Test + void avoidCollisionWithSanitization() { + final String name = "name_b"; + + final Map entries = new HashMap<>(); + addNewEntry(newEntry(name, Type.STRING), entries); + addNewEntry(newEntry(name, Type.INT), entries); + + // second entry with the same name was ignored (can't be two same raw names) + Assertions.assertEquals(1, entries.size()); + + Assertions.assertNull(entries.get(name).getRawName()); + Assertions.assertEquals(name, entries.get("name_b").getName()); + // we remain the first entry. + Assertions.assertEquals(Type.STRING, entries.get("name_b").getType()); + } + + @Test + void avoidCollisionEqualLengthCyrillicNames() { + final String firstRawName = "Світло"; + final String secondRawName = "Мріяти"; + final String thirdRawName = "Копати"; + + final Map entries = new HashMap<>(); + addNewEntry(newEntry(firstRawName, Type.STRING), entries); + addNewEntry(newEntry(secondRawName, Type.STRING), entries); + addNewEntry(newEntry(thirdRawName, Type.STRING), entries); + + Assertions.assertEquals(3, entries.size()); + + // Check that the sanitized names are different + // it was a strange behavior when we replace the existed entry with the same name + Assertions.assertEquals(thirdRawName, entries.get("_____").getRawName()); + Assertions.assertEquals(secondRawName, entries.get("______2").getRawName()); + Assertions.assertEquals(firstRawName, entries.get("______1").getRawName()); + } + + @Test + void avoidCollisionNormalNameFirst() { + final String firstRawName = "name_b"; + final String secondRawName = "1name_b"; + + final Map entries = new HashMap<>(); + addNewEntry(newEntry(firstRawName, Type.STRING), entries); + addNewEntry(newEntry(secondRawName, Type.STRING), entries); + + Assertions.assertEquals(2, entries.size()); + + // Check that the sanitized names are different + // it was a strange behavior when we replace the existed entry with the same name + Assertions.assertNull(entries.get("name_b").getRawName()); + Assertions.assertEquals(firstRawName, entries.get("name_b").getName()); + Assertions.assertEquals(secondRawName, entries.get("name_b_1").getRawName()); + } + + @Test + void avoidCollisionNormalNameLast() { + final String firstRawName = "1name_b"; + final String secondRawName = "name_b"; + + final Map entries = new HashMap<>(); + addNewEntry(newEntry(firstRawName, Type.STRING), entries); + addNewEntry(newEntry(secondRawName, Type.STRING), entries); + + Assertions.assertEquals(2, entries.size()); + + // Check that the sanitized names are different + // it was a strange behavior when we replace the existed entry with the same name + Assertions.assertEquals(firstRawName, entries.get("name_b_1").getRawName()); + Assertions.assertNull(entries.get("name_b").getRawName()); + Assertions.assertEquals(secondRawName, entries.get("name_b").getName()); + } + + private static Schema.Entry newEntry(final String name, final Schema.Type type) { + final String sanitizedName = SchemaCompanionUtil.sanitizeName(name); + return MockEntry.internalBuilder() + .withName(sanitizedName) + .withRawName(name.equals(sanitizedName) ? null : name) + .withType(type) + .build(); + } + + private static void addNewEntry(final Entry entry, final Map entries) { + final ReplaceFunction replaceFunction = new ReplaceFunction(entries); + final Entry sanitized = SchemaCompanionUtil.avoidCollision(entry, entries::get, replaceFunction); + if (sanitized != null) { + entries.put(sanitized.getName(), sanitized); + } + } + + @RequiredArgsConstructor + private static final class ReplaceFunction implements BiConsumer { + + private final Map entries; + + @Override + public void accept(final String s, final Entry entry) { + entries.remove(s); + entries.put(entry.getName(), entry); + } + } +} diff --git a/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaTest.java b/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaTest.java index d1b8e6bb8094b..a89ab31da6ad6 100644 --- a/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaTest.java +++ b/component-api/src/test/java/org/talend/sdk/component/api/record/SchemaTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; @@ -29,8 +28,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Random; -import java.util.regex.Pattern; import java.util.stream.Stream; import javax.json.Json; @@ -84,45 +81,6 @@ void testJsonProp() { Assertions.assertEquals(2, value3.asJsonArray().getJsonNumber(1).intValue()); } - @Test - void testSanitize() { - Assertions.assertNull(Schema.sanitizeConnectionName(null)); - Assertions.assertEquals("", Schema.sanitizeConnectionName("")); - Assertions.assertEquals("_", Schema.sanitizeConnectionName("$")); - Assertions.assertEquals("_", Schema.sanitizeConnectionName("1")); - Assertions.assertEquals("_", Schema.sanitizeConnectionName("é")); - Assertions.assertEquals("H", Schema.sanitizeConnectionName("éH")); - Assertions.assertEquals("_1", Schema.sanitizeConnectionName("é1")); - Assertions.assertEquals("H_lloWorld", Schema.sanitizeConnectionName("HélloWorld")); - Assertions.assertEquals("oid", Schema.sanitizeConnectionName("$oid")); - Assertions.assertEquals("Hello_World_", Schema.sanitizeConnectionName(" Hello World ")); - Assertions.assertEquals("_23HelloWorld", Schema.sanitizeConnectionName("123HelloWorld")); - - Assertions.assertEquals("Hello_World_", Schema.sanitizeConnectionName("Hello-World$")); - - final Pattern checkPattern = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$"); - final String nonAscii1 = Schema.sanitizeConnectionName("30_39歳"); - Assertions.assertTrue(checkPattern.matcher(nonAscii1).matches(), "'" + nonAscii1 + "' don't match"); - - final String ch1 = Schema.sanitizeConnectionName("世帯数分布"); - final String ch2 = Schema.sanitizeConnectionName("抽出率調整"); - Assertions.assertTrue(checkPattern.matcher(ch1).matches(), "'" + ch1 + "' don't match"); - Assertions.assertTrue(checkPattern.matcher(ch2).matches(), "'" + ch2 + "' don't match"); - Assertions.assertNotEquals(ch1, ch2); - - final Random rnd = new Random(); - final byte[] array = new byte[20]; // length is bounded by 7 - for (int i = 0; i < 150; i++) { - rnd.nextBytes(array); - final String randomString = new String(array, StandardCharsets.UTF_8); - final String sanitize = Schema.sanitizeConnectionName(randomString); - Assertions.assertTrue(checkPattern.matcher(sanitize).matches(), "'" + sanitize + "' don't match"); - - final String sanitize2 = Schema.sanitizeConnectionName(sanitize); - Assertions.assertEquals(sanitize, sanitize2); - } - } - @Test void testTypes() { final Record rec = new Record() { diff --git a/component-api/src/test/java/org/talend/sdk/component/api/service/record/RecordBuilderFactoryTest.java b/component-api/src/test/java/org/talend/sdk/component/api/service/record/RecordBuilderFactoryTest.java index ff2c4fba06b14..12cd303d22fe3 100644 --- a/component-api/src/test/java/org/talend/sdk/component/api/service/record/RecordBuilderFactoryTest.java +++ b/component-api/src/test/java/org/talend/sdk/component/api/service/record/RecordBuilderFactoryTest.java @@ -25,9 +25,8 @@ import org.talend.sdk.component.api.record.Schema; import org.talend.sdk.component.api.record.Schema.Entry; import org.talend.sdk.component.api.record.Schema.Type; -import org.talend.sdk.component.api.service.record.RecordBuilderFactoryTest.MockEntry.MockEntryBuilder; - -import lombok.RequiredArgsConstructor; +import org.talend.sdk.component.api.test.MockEntry; +import org.talend.sdk.component.api.test.MockEntry.MockEntryBuilder; class RecordBuilderFactoryTest { @@ -60,135 +59,7 @@ public Schema.Builder newSchemaBuilder(Schema schema) { @Override public Entry.Builder newEntryBuilder() { - return new MockTCKEntryBuilder(MockEntry.builder()); - } - } - - @lombok.Builder(setterPrefix = "with") - @lombok.Getter - static class MockEntry implements Schema.Entry { - - private final String name; - - private final String rawName; - - private final String originalFieldName; - - private final Type type; - - private final boolean nullable; - - private final boolean errorCapable; - - private final boolean metadata; - - private final Object defaultVal; - - @Override - public T getDefaultValue() { - return (T) defaultVal; - } - - private final Schema elementSchema; - - private final String comment; - - private final Map props; - - @Override - public String getProp(final String property) { - return this.getProps().get(property); - } - - @Override - public Builder toBuilder() { - throw new UnsupportedOperationException("#toBuilder()"); - } - - @Override - public boolean isValid() { - return true; - } - } - - @RequiredArgsConstructor - static class MockTCKEntryBuilder implements Entry.Builder { - - private final Map props = new HashMap<>(); - - private final MockEntryBuilder builder; - - @Override - public Entry.Builder withName(String name) { - builder.withName(name); - return this; - } - - @Override - public Entry.Builder withRawName(String rawName) { - this.builder.withRawName(rawName); - return this; - } - - @Override - public Entry.Builder withType(Type type) { - this.builder.withType(type); - return this; - } - - @Override - public Entry.Builder withNullable(boolean nullable) { - this.builder.withNullable(nullable); - return this; - } - - @Override - public Entry.Builder withErrorCapable(boolean errorCapable) { - this.builder.withErrorCapable(errorCapable); - return this; - } - - @Override - public Entry.Builder withMetadata(boolean metadata) { - this.builder.withMetadata(metadata); - return this; - } - - @Override - public Entry.Builder withDefaultValue(T value) { - builder.withDefaultVal(value); - return this; - } - - @Override - public Entry.Builder withElementSchema(Schema schema) { - builder.withElementSchema(schema); - return this; - } - - @Override - public Entry.Builder withComment(String comment) { - this.builder.withComment(comment); - return this; - } - - @Override - public Entry.Builder withProps(Map inProps) { - this.props.clear(); - this.props.putAll(inProps); - return this; - } - - @Override - public Entry.Builder withProp(String key, String value) { - this.props.put(key, value); - return this; - } - - @Override - public Entry build() { - builder.withProps(this.props); - return builder.build(); + return new MockEntryBuilder(MockEntry.internalBuilder()); } } @@ -200,7 +71,7 @@ void newEntryBuilder() { props.put("p1", "v1"); Schema.Entry e1 = MockEntry - .builder() // + .internalBuilder() // .withName("n1") // .withDefaultVal("default") // .withType(Type.STRING) // @@ -227,6 +98,6 @@ void newEntryBuilder() { Assertions.assertEquals("v1", e1.getProp("p1")); Assertions.assertEquals("v2", e2.getProp("p2")); - Assertions.assertEquals(null, e1.getProp("p2")); + Assertions.assertNull(e1.getProp("p2")); } } \ No newline at end of file diff --git a/component-api/src/test/java/org/talend/sdk/component/api/test/MockEntry.java b/component-api/src/test/java/org/talend/sdk/component/api/test/MockEntry.java new file mode 100644 index 0000000000000..25bc357ef4bcf --- /dev/null +++ b/component-api/src/test/java/org/talend/sdk/component/api/test/MockEntry.java @@ -0,0 +1,169 @@ +/** + * 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.api.test; + +import java.util.HashMap; +import java.util.Map; + +import org.talend.sdk.component.api.record.Schema; +import org.talend.sdk.component.api.record.Schema.Entry; +import org.talend.sdk.component.api.record.Schema.Type; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +@EqualsAndHashCode +@lombok.Builder(setterPrefix = "with", + builderMethodName = "internalBuilder", + builderClassName = "MockEntryInternalBuilder") +@lombok.Getter +public class MockEntry implements Schema.Entry { + + private final String name; + + private final String rawName; + + private final String originalFieldName; + + private final Type type; + + private final boolean nullable; + + private final boolean errorCapable; + + private final boolean metadata; + + private final Object defaultVal; + + @Override + public T getDefaultValue() { + return (T) defaultVal; + } + + private final Schema elementSchema; + + private final String comment; + + private final Map props; + + @Override + public String getProp(final String property) { + return this.getProps().get(property); + } + + @Override + public Builder toBuilder() { + final MockEntryInternalBuilder builder = internalBuilder() + .withName(name) + .withRawName(rawName) + .withOriginalFieldName(originalFieldName) + .withType(type) + .withNullable(nullable) + .withErrorCapable(errorCapable) + .withMetadata(metadata) + .withDefaultVal(defaultVal) + .withElementSchema(elementSchema) + .withComment(comment) + .withProps(props == null ? null : new HashMap<>(props)); + return new MockEntryBuilder(builder); + } + + @Override + public boolean isValid() { + return true; + } + + @RequiredArgsConstructor + public static class MockEntryBuilder implements Builder { + + private final Map props = new HashMap<>(); + + private final MockEntryInternalBuilder builder; + + @Override + public Builder withName(String name) { + builder.withName(name); + return this; + } + + @Override + public Builder withRawName(String rawName) { + this.builder.withRawName(rawName); + return this; + } + + @Override + public Builder withType(Type type) { + this.builder.withType(type); + return this; + } + + @Override + public Builder withNullable(boolean nullable) { + this.builder.withNullable(nullable); + return this; + } + + @Override + public Builder withErrorCapable(boolean errorCapable) { + this.builder.withErrorCapable(errorCapable); + return this; + } + + @Override + public Builder withMetadata(boolean metadata) { + this.builder.withMetadata(metadata); + return this; + } + + @Override + public Builder withDefaultValue(T value) { + builder.withDefaultVal(value); + return this; + } + + @Override + public Builder withElementSchema(Schema schema) { + builder.withElementSchema(schema); + return this; + } + + @Override + public Builder withComment(String comment) { + this.builder.withComment(comment); + return this; + } + + @Override + public Builder withProps(Map inProps) { + this.props.clear(); + this.props.putAll(inProps); + return this; + } + + @Override + public Builder withProp(String key, String value) { + this.props.put(key, value); + return this; + } + + @Override + public Entry build() { + builder.withProps(this.props); + return builder.build(); + } + } +} diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/BaseProcessorFn.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/BaseProcessorFn.java index 8fb92cc22fb24..8cc1f509bd282 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/BaseProcessorFn.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/BaseProcessorFn.java @@ -17,7 +17,7 @@ import static java.util.Collections.emptyIterator; import static java.util.stream.Collectors.toMap; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import java.util.ArrayList; import java.util.Collection; @@ -154,7 +154,7 @@ protected static final class BeamInputFactory implements InputFactory { @Override public Object read(final String name) { - final Iterator values = objects.getOrDefault(sanitizeConnectionName(name), emptyIterator()); + final Iterator values = objects.getOrDefault(sanitizeName(name), emptyIterator()); return values.hasNext() ? values.next() : null; } } @@ -172,7 +172,7 @@ protected abstract static class BeamOutputFactory implements OutputFactory { @Override public OutputEmitter create(final String name) { - return new BeamOutputEmitter(outputs.computeIfAbsent(sanitizeConnectionName(name), k -> new ArrayList<>()), + return new BeamOutputEmitter(outputs.computeIfAbsent(sanitizeName(name), k -> new ArrayList<>()), factory, jsonb); } @@ -190,7 +190,7 @@ protected BeamSingleOutputFactory(final Consumer emit, final RecordBuild @Override public OutputEmitter create(final String name) { - return new BeamOutputEmitter(outputs.computeIfAbsent(sanitizeConnectionName(name), k -> new ArrayList<>()), + return new BeamOutputEmitter(outputs.computeIfAbsent(sanitizeName(name), k -> new ArrayList<>()), factory, jsonb); } diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecord.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecord.java index 41b0b164426dd..b170d42e88846 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecord.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecord.java @@ -17,7 +17,7 @@ import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import static org.talend.sdk.component.runtime.beam.avro.AvroSchemas.unwrapUnion; import java.math.BigDecimal; @@ -88,14 +88,14 @@ public AvroRecord(final Record record) { record .getSchema() .getAllEntries() - .forEach(entry -> ofNullable(record.get(Object.class, sanitizeConnectionName(entry.getName()))) + .forEach(entry -> ofNullable(record.get(Object.class, sanitizeName(entry.getName()))) .ifPresent(v -> { final Object avroValue = directMapping(v, entry); if (avroValue != null) { final org.apache.avro.Schema.Field field = this.schema.getActualDelegate() - .getField(sanitizeConnectionName(entry.getName())); + .getField(sanitizeName(entry.getName())); delegate.put(field.pos(), avroValue); } })); @@ -167,7 +167,7 @@ public T get(final Class expectedType, final String name) { if (expectedType == Collection.class) { return expectedType.cast(getArray(Object.class, name)); } - return doGet(expectedType, sanitizeConnectionName(name)); + return doGet(expectedType, sanitizeName(name)); } @Override @@ -180,7 +180,7 @@ public T get(final Class expectedType, final Schema.Entry entry) { @Override public Collection getArray(final Class type, final String name) { - final String sanitizedName = sanitizeConnectionName(name); + final String sanitizedName = sanitizeName(name); return this.doGetArray(type, sanitizedName); } diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroSchemaBuilder.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroSchemaBuilder.java index e234a3a031ba7..2c13cc9de1a25 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroSchemaBuilder.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/spi/record/AvroSchemaBuilder.java @@ -17,7 +17,7 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import static org.talend.sdk.component.runtime.record.SchemaImpl.ENTRIES_ORDER_PROP; import static org.talend.sdk.component.runtime.record.Schemas.EMPTY_RECORD; @@ -36,6 +36,7 @@ import org.talend.sdk.component.api.record.Schema; import org.talend.sdk.component.api.record.Schema.Builder; import org.talend.sdk.component.api.record.Schema.Entry; +import org.talend.sdk.component.api.record.SchemaCompanionUtil; import org.talend.sdk.component.api.record.SchemaProperty; import org.talend.sdk.component.runtime.beam.avro.AvroSchemas; import org.talend.sdk.component.runtime.manager.service.api.Unwrappable; @@ -181,7 +182,7 @@ public Schema.Builder withEntry(final Schema.Entry entry) { fields = new OrderedMap<>(Schema.Entry::getName, Collections.singletonList(entry)); } - final Schema.Entry realEntry = Schema.avoidCollision(entry, fields::getValue, fields::replace); + final Schema.Entry realEntry = SchemaCompanionUtil.avoidCollision(entry, fields::getValue, fields::replace); fields.addValue(realEntry); return this; } @@ -442,13 +443,13 @@ public static class AvroHelper { public static Field toField(final org.apache.avro.Schema schema, final Schema.Entry entry) { Field field = null; try { - field = new Field(sanitizeConnectionName(entry.getName()), + field = new Field(sanitizeName(entry.getName()), entry.isNullable() && schema.getType() != Type.UNION ? org.apache.avro.Schema.createUnion(asList(schema, NULL_SCHEMA)) : schema, entry.getComment(), (Object) entry.getDefaultValue()); } catch (AvroTypeException e) { - field = new Field(sanitizeConnectionName(entry.getName()), + field = new Field(sanitizeName(entry.getName()), entry.isNullable() && schema.getType() != Type.UNION ? org.apache.avro.Schema.createUnion(asList(schema, NULL_SCHEMA)) : schema, diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchFilter.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchFilter.java index 327f9903acb29..c6778dfd20835 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchFilter.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchFilter.java @@ -15,7 +15,7 @@ */ package org.talend.sdk.component.runtime.beam.transform; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import java.util.Collection; @@ -41,7 +41,7 @@ public class RecordBranchFilter extends DoFn { public RecordBranchFilter(final RecordBuilderFactory factory, final String branch) { this.factory = factory; - this.branch = sanitizeConnectionName(branch); + this.branch = sanitizeName(branch); } protected RecordBranchFilter() { diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchMapper.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchMapper.java index 16b844df64d42..5231dc1189500 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchMapper.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchMapper.java @@ -15,7 +15,7 @@ */ package org.talend.sdk.component.runtime.beam.transform; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import java.util.Collection; @@ -44,8 +44,8 @@ public class RecordBranchMapper extends DoFn { public RecordBranchMapper(final RecordBuilderFactory factory, final String sourceBranch, final String targetBranch) { this.factory = factory; - this.sourceBranch = sanitizeConnectionName(sourceBranch); - this.targetBranch = sanitizeConnectionName(targetBranch); + this.sourceBranch = sanitizeName(sourceBranch); + this.targetBranch = sanitizeName(targetBranch); } protected RecordBranchMapper() { diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchUnwrapper.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchUnwrapper.java index cbcc7370609ea..be75b115ae881 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchUnwrapper.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/RecordBranchUnwrapper.java @@ -15,7 +15,7 @@ */ package org.talend.sdk.component.runtime.beam.transform; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import java.util.Collection; @@ -33,7 +33,7 @@ public class RecordBranchUnwrapper extends DoFn { private String branch; public RecordBranchUnwrapper(final String branch) { - this.branch = sanitizeConnectionName(branch); + this.branch = sanitizeName(branch); } protected RecordBranchUnwrapper() { diff --git a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/ViewsMappingTransform.java b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/ViewsMappingTransform.java index 162585c72a281..0870119dc8e53 100644 --- a/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/ViewsMappingTransform.java +++ b/component-runtime-beam/src/main/java/org/talend/sdk/component/runtime/beam/transform/ViewsMappingTransform.java @@ -19,7 +19,7 @@ import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toMap; import static lombok.AccessLevel.PROTECTED; -import static org.talend.sdk.component.api.record.Schema.sanitizeConnectionName; +import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName; import java.util.Map; @@ -77,7 +77,7 @@ private MappingViewsFn(final Map> views, final String this.views = views .entrySet() .stream() - .collect(toMap(e -> sanitizeConnectionName(e.getKey()), Map.Entry::getValue)); + .collect(toMap(e -> sanitizeName(e.getKey()), Map.Entry::getValue)); this.plugin = plugin; } diff --git a/component-runtime-beam/src/test/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecordTest.java b/component-runtime-beam/src/test/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecordTest.java index ba3006513a4be..b4fbefc5aaacc 100644 --- a/component-runtime-beam/src/test/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecordTest.java +++ b/component-runtime-beam/src/test/java/org/talend/sdk/component/runtime/beam/spi/record/AvroRecordTest.java @@ -84,6 +84,7 @@ import org.talend.sdk.component.api.record.Record; import org.talend.sdk.component.api.record.Schema; import org.talend.sdk.component.api.record.Schema.Entry; +import org.talend.sdk.component.api.record.SchemaCompanionUtil; import org.talend.sdk.component.api.record.SchemaProperty; import org.talend.sdk.component.api.service.record.RecordBuilderFactory; import org.talend.sdk.component.runtime.beam.coder.registry.SchemaRegistryCoder; @@ -100,7 +101,8 @@ class AvroRecordTest { * Avro logical type must be an int and contains milliseconds from 00:00:00. * * Please have a look to Avro specification: - * {@link Timestamp}. + * {@link Timestamp}. * *
      * Time (millisecond precision)
@@ -1059,24 +1061,32 @@ void testConstructor() {
     }
 
     @Test
-    void recordCollisionName() {
+    void collisionWithAcceptedNamesAndDifferentTypes() {
         // Case with collision without sanitize.
         final Record record = new AvroRecordBuilder() //
                 .withString("field", "value1") //
                 .withInt("field", 234) //
                 .build();
         final Object value = record.get(Object.class, "field");
-        Assertions.assertEquals(Integer.valueOf(234), value);
+        Assertions.assertEquals(234, value);
+    }
 
+    @Test
+    void recordCollisionName() {
         // Case with collision and sanitize.
-        final Record recordSanitize = new AvroRecordBuilder() //
-                .withString("70歳以上", "value70") //
-                .withString("60歳以上", "value60") //
+        final Record recordSanitize = new AvroRecordBuilder()
+                .withString("70歳以上", "value70")
+                .withString("60歳以上", "value60")
                 .build();
+
         Assertions.assertEquals(2, recordSanitize.getSchema().getEntries().size());
-        final String name1 = Schema.sanitizeConnectionName("70歳以上");
-        Assertions.assertEquals("value70", recordSanitize.getString(name1));
-        Assertions.assertEquals("value60", recordSanitize.getString(name1 + "_1"));
+
+        // both names are sanitized to the one name, but with replacement mechanism inside and prefixes the ordering
+        // will be changed
+        // last entered will take the simpler name
+        final String sanitizedName = SchemaCompanionUtil.sanitizeName("70歳以上");
+        Assertions.assertEquals("value60", recordSanitize.getString(sanitizedName));
+        Assertions.assertEquals("value70", recordSanitize.getString(sanitizedName + "_1"));
     }
 
     @Test
diff --git a/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/RecordImpl.java b/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/RecordImpl.java
index 5f6dcc8933753..5ca694666e359 100644
--- a/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/RecordImpl.java
+++ b/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/RecordImpl.java
@@ -61,6 +61,7 @@
 import org.talend.sdk.component.api.record.Schema;
 import org.talend.sdk.component.api.record.Schema.EntriesOrder;
 import org.talend.sdk.component.api.record.Schema.Entry;
+import org.talend.sdk.component.api.record.SchemaCompanionUtil;
 import org.talend.sdk.component.api.record.SchemaProperty;
 
 import lombok.EqualsAndHashCode;
@@ -579,10 +580,9 @@ private  Builder append(final Schema.Entry entry, final T value) {
 
             final Schema.Entry realEntry;
             if (this.entries != null) {
-                realEntry = Optional
-                        .ofNullable(Schema.avoidCollision(entry,
-                                this.entries::getValue,
-                                this.entries::replace))
+                realEntry = Optional.ofNullable(
+                        SchemaCompanionUtil.avoidCollision(entry, this.entries::getValue,
+                                this::replaceEntryAndItsValue))
                         .orElse(entry);
             } else {
                 realEntry = entry;
@@ -610,6 +610,17 @@ private  Builder append(final Schema.Entry entry, final T value) {
             return this;
         }
 
+        /**
+         * Replace the entry in the entries map and update the values map with the new entry name.
+         * Because of the logic that new entry will use the simple sanitized name,
+         * and we should rename the previous entry that used that name before.
+         */
+        private void replaceEntryAndItsValue(final String id, final Entry updatedEntry) {
+            entries.replace(id, updatedEntry);
+            final Object prevValue = values.remove(id);
+            values.put(updatedEntry.getName(), prevValue);
+        }
+
         private enum Order {
             BEFORE,
             AFTER,
diff --git a/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/SchemaImpl.java b/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/SchemaImpl.java
index 41886e2ee38b2..bc8d9b5b98a91 100644
--- a/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/SchemaImpl.java
+++ b/component-runtime-impl/src/main/java/org/talend/sdk/component/runtime/record/SchemaImpl.java
@@ -34,6 +34,7 @@
 
 import org.talend.sdk.component.api.record.OrderedMap;
 import org.talend.sdk.component.api.record.Schema;
+import org.talend.sdk.component.api.record.SchemaCompanionUtil;
 import org.talend.sdk.component.api.record.SchemaProperty;
 
 import lombok.EqualsAndHashCode;
@@ -200,20 +201,20 @@ public Builder withEntry(final Entry entry) {
             if (type != Type.RECORD) {
                 throw new IllegalArgumentException("entry is only valid for RECORD type of schema");
             }
-            final Entry entryToAdd = Schema.avoidCollision(entry,
+            final Entry entryToAdd = SchemaCompanionUtil.avoidCollision(entry,
                     this::getEntry,
                     this::replaceEntry);
             if (entryToAdd == null) {
                 // mean try to add entry with same name.
                 throw new IllegalArgumentException("Entry with name " + entry.getName() + " already exist in schema");
             }
-            if (entry.isMetadata()) {
+            if (entryToAdd.isMetadata()) {
                 this.metadataEntries.addValue(entryToAdd);
             } else {
                 this.entries.addValue(entryToAdd);
             }
 
-            entriesOrder.add(entry.getName());
+            entriesOrder.add(entryToAdd.getName());
             return this;
         }
 
@@ -230,7 +231,7 @@ public Builder withEntryBefore(final String before, final Entry entry) {
         }
 
         private void replaceEntry(final String name, final Schema.Entry entry) {
-            if (this.entries.getValue(entry.getName()) != null) {
+            if (this.entries.getValue(name) != null) {
                 this.entries.replace(name, entry);
             } else if (this.metadataEntries.getValue(name) != null) {
                 this.metadataEntries.replace(name, entry);
@@ -529,7 +530,7 @@ private BuilderImpl(final Entry entry) {
             }
 
             public Builder withName(final String name) {
-                this.name = Schema.sanitizeConnectionName(name);
+                this.name = SchemaCompanionUtil.sanitizeName(name);
                 // if raw name is changed as follow name rule, use label to store raw name
                 // if not changed, not set label to save space
                 if (!name.equals(this.name)) {
diff --git a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderFactoryImplTest.java b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderFactoryImplTest.java
index 7fe303e35bddd..69c3660047ff2 100644
--- a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderFactoryImplTest.java
+++ b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderFactoryImplTest.java
@@ -33,6 +33,7 @@
 import org.junit.jupiter.api.TestInstance;
 import org.talend.sdk.component.api.record.Record;
 import org.talend.sdk.component.api.record.Schema;
+import org.talend.sdk.component.api.record.Schema.Entry;
 import org.talend.sdk.component.api.service.record.RecordBuilderFactory;
 import org.talend.sdk.component.runtime.serialization.DynamicContainerFinder;
 
@@ -109,4 +110,33 @@ void serial() throws IOException, ClassNotFoundException {
             Assertions.assertNotNull(factory2);
         }
     }
+
+    @Test
+    void sanitizeCyrillicEqualLength() {
+        final String firstRawName = "Світло";
+        final String secondRawName = "Мріяти";
+
+        final Record record = factory.newRecordBuilder()
+                .withString(firstRawName, firstRawName)
+                .withString(secondRawName, secondRawName)
+                .build();
+
+        String sanitized1 = findNameByRawName(firstRawName, record);
+        String value1 = record.getString(sanitized1);
+        Assertions.assertEquals(firstRawName, value1);
+
+        String sanitized2 = findNameByRawName(secondRawName, record);
+        String value2 = record.getString(sanitized2);
+        Assertions.assertEquals(secondRawName, value2);
+    }
+
+    private static String findNameByRawName(final String n, final Record record) {
+        return record.getSchema()
+                .getEntries()
+                .stream()
+                .filter(e -> e.getRawName().equals(n))
+                .map(Entry::getName)
+                .findAny()
+                .orElseThrow(AssertionError::new);
+    }
 }
diff --git a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderImplTest.java b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderImplTest.java
index 85e233722a835..0300000cf8e59 100644
--- a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderImplTest.java
+++ b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/RecordBuilderImplTest.java
@@ -48,6 +48,7 @@
 import org.talend.sdk.component.api.record.Schema.EntriesOrder;
 import org.talend.sdk.component.api.record.Schema.Entry;
 import org.talend.sdk.component.api.record.Schema.Type;
+import org.talend.sdk.component.api.record.SchemaCompanionUtil;
 import org.talend.sdk.component.api.record.SchemaProperty;
 import org.talend.sdk.component.runtime.record.SchemaImpl.BuilderImpl;
 import org.talend.sdk.component.runtime.record.SchemaImpl.EntryImpl;
@@ -848,22 +849,31 @@ void withRecordFromNameWithNullValue() {
     }
 
     @Test
-    void testSimpleCollision() {
-        final Record record = new RecordImpl.BuilderImpl() //
-                .withString("goodName", "v1") //
-                .withString("goodName", "v2") //
+    void collisionWithSameNameAndType() {
+        // Case with collision without sanitize.
+        final Record record = new RecordImpl.BuilderImpl()
+                .withString("goodName", "v1")
+                .withString("goodName", "v2")
                 .build();
+        Assertions.assertEquals(1, record.getSchema().getEntries().size());
         Assertions.assertEquals("v2", record.getString("goodName"));
+    }
 
+    @Test
+    void simpleCollision() {
         // Case with collision and sanitize.
-        final Record recordSanitize = new RecordImpl.BuilderImpl() //
-                .withString("70歳以上", "value70") //
-                .withString("60歳以上", "value60") //
+        final Record recordSanitize = new RecordImpl.BuilderImpl()
+                .withString("70歳以上", "value70")
+                .withString("60歳以上", "value60")
                 .build();
         Assertions.assertEquals(2, recordSanitize.getSchema().getEntries().size());
-        final String name1 = Schema.sanitizeConnectionName("70歳以上");
-        Assertions.assertEquals("value70", recordSanitize.getString(name1));
-        Assertions.assertEquals("value60", recordSanitize.getString(name1 + "_1"));
+
+        // both names are sanitized to the one name, but with replacement mechanism inside and prefixes the ordering
+        // will be changed
+        // last entered will take the simpler name
+        final String name1 = SchemaCompanionUtil.sanitizeName("70歳以上");
+        Assertions.assertEquals("value60", recordSanitize.getString(name1));
+        Assertions.assertEquals("value70", recordSanitize.getString(name1 + "_1"));
     }
 
     @Test
diff --git a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaCompanionUtilTest.java b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaCompanionUtilTest.java
new file mode 100644
index 0000000000000..02201516eedbf
--- /dev/null
+++ b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaCompanionUtilTest.java
@@ -0,0 +1,112 @@
+/**
+ * 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.runtime.record;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.talend.sdk.component.api.record.Schema;
+import org.talend.sdk.component.api.record.Schema.Entry;
+import org.talend.sdk.component.api.record.Schema.Type;
+import org.talend.sdk.component.api.record.SchemaCompanionUtil;
+
+import lombok.RequiredArgsConstructor;
+
+class SchemaCompanionUtilTest {
+
+    @RequiredArgsConstructor
+    private static final class ReplaceFunction implements BiConsumer {
+
+        private final Map entries;
+
+        @Override
+        public void accept(final String s, final Entry entry) {
+            entries.remove(s);
+            entries.put(entry.getName(), entry);
+        }
+    }
+
+    @Test
+    void testAvoidCollision() {
+        final Map entries = new HashMap<>();
+        for (int index = 1; index < 8; index++) {
+            addNewEntry(newEntry(index + "name_b", Type.STRING), entries);
+        }
+
+        // this one should collide with the previous ones
+        addNewEntry(newEntry("name_b_5", Type.STRING), entries);
+
+        // in total 7 + 1 = 8 entries
+        Assertions.assertEquals(8, entries.size());
+        Assertions.assertEquals("name_b", entries.get("name_b").getName());
+        // 7 names with suffixes
+        Assertions
+                .assertTrue(IntStream
+                        .range(1, 8)
+                        .mapToObj((int i) -> "name_b_" + i)
+                        .allMatch((String name) -> entries.get(name).getName().equals(name)));
+    }
+
+    @Test
+    void collisionDuplicatedNormalName() {
+        final Map entriesDuplicate = new HashMap<>();
+        final Schema.Entry e1 = newEntry("goodName", Type.STRING);
+        final Schema.Entry realEntry1 = addNewEntry(e1, entriesDuplicate);
+        Assertions.assertSame(e1, realEntry1);
+
+        final Schema.Entry e2 = newEntry("goodName", Type.STRING);
+        final Schema.Entry realEntry2 = addNewEntry(e2, entriesDuplicate);
+
+        Assertions.assertSame(realEntry2, e2);
+    }
+
+    /**
+     * Similar to, but with the real EntryImpl that has build-in sanitization.
+     * org.talend.sdk.component.api.record.SchemaCompanionUtilTest#sanitizeEqualLengthCyrillicNames()
+     */
+    @Test
+    void avoidCollisionEqualLengthCyrillicNames() {
+        final String firstRawName = "Світло";
+        final String secondRawName = "Мріяти";
+
+        final Map entries = new HashMap<>();
+
+        addNewEntry(newEntry(firstRawName, Type.STRING), entries);
+        addNewEntry(newEntry(secondRawName, Type.STRING), entries);
+
+        Assertions.assertEquals(2, entries.size());
+
+        // Check that the sanitized names are different
+        // it was a strange behavior when we replace the existed entry with the same name
+        Assertions.assertEquals(secondRawName, entries.get("_____").getRawName());
+        Assertions.assertEquals(firstRawName, entries.get("______1").getRawName());
+    }
+
+    private static Entry addNewEntry(final Entry entry, final Map entries) {
+        final ReplaceFunction replaceFunction = new ReplaceFunction(entries);
+        final Entry sanitized = SchemaCompanionUtil.avoidCollision(entry, entries::get, replaceFunction);
+        entries.put(sanitized.getName(), sanitized);
+        return sanitized;
+    }
+
+    private static Schema.Entry newEntry(final String name, final Schema.Type type) {
+        return new SchemaImpl.EntryImpl.BuilderImpl().withName(name).withType(type).build();
+    }
+}
diff --git a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaImplTest.java b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaImplTest.java
index 3e447fadb511f..c7a6b0c685c06 100644
--- a/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaImplTest.java
+++ b/component-runtime-impl/src/test/java/org/talend/sdk/component/runtime/record/SchemaImplTest.java
@@ -188,34 +188,55 @@ void testRecordWithMetadataFields() {
                 .build();
         Schema recordSchema = record.getSchema();
         Assertions.assertEquals(1, recordSchema.getEntries().size());
-        Assertions.assertEquals(2, recordSchema.getAllEntries().filter(e -> e.isMetadata()).count());
+        Assertions.assertEquals(2, recordSchema.getAllEntries().filter(Entry::isMetadata).count());
         Assertions.assertEquals(34, record.getInt("record_id"));
         Assertions.assertEquals("Aloa", record.getString("field1"));
         Assertions.assertEquals("Hallo, wie gehst du ?", record.getString("field2"));
     }
 
+    @Test
+    void antiCollisionRealNameFirst() {
+        final Entry entry1 = newEntry("name_b", "c_value");
+        final Entry entry2 = newEntry("1name_b", "a_value");
+        final Entry entry3 = newEntry("2name_b", "b_value");
+
+        final Schema schema = newSchema(entry1, entry2, entry3);
+
+        final boolean checkNames = schema
+                .getAllEntries()
+                .allMatch(e -> ("1name_b".equals(e.getRawName()) && e.getName().matches("name_b_[12]")
+                        && "a_value".equals(e.getDefaultValue()))
+                        || ("2name_b".equals(e.getRawName()) && e.getName().matches("name_b_[12]")
+                                && "b_value".equals(e.getDefaultValue()))
+                        || (e.getRawName() == null && e.getName().equals("name_b")
+                                && "c_value".equals(e.getDefaultValue())));
+
+        Assertions.assertTrue(checkNames);
+        Assertions.assertEquals(3, schema.getAllEntries().map(Entry::getName).distinct().count());
+    }
+
     @Test
     void testAntiCollision() {
-        final Entry entry1 = this.newEntry("1name_b", "a_value");
-        final Entry entry2 = this.newEntry("2name_b", "b_value");
-        final Entry entry3 = this.newEntry("name_b", "c_value");
+        final Entry entry1 = newEntry("1name_b", "a_value");
+        final Entry entry2 = newEntry("2name_b", "b_value");
+        final Entry entry3 = newEntry("name_b", "c_value");
 
-        final Schema schema = this.newSchema(entry1, entry2, entry3);
+        final Schema schema = newSchema(entry1, entry2, entry3);
 
         final boolean checkNames = schema
                 .getAllEntries()
-                .allMatch((Entry e) -> ("1name_b".equals(e.getRawName()) && e.getName().matches("name_b_[12]")
-                        && "a_value".equals(e.getDefaultValue())) //
+                .allMatch(e -> ("1name_b".equals(e.getRawName()) && e.getName().matches("name_b_[12]")
+                        && "a_value".equals(e.getDefaultValue()))
                         || ("2name_b".equals(e.getRawName()) && e.getName().matches("name_b_[12]")
-                                && "b_value".equals(e.getDefaultValue())) //
+                                && "b_value".equals(e.getDefaultValue()))
                         || (e.getRawName() == null && e.getName().equals("name_b")
                                 && "c_value".equals(e.getDefaultValue())));
         Assertions.assertTrue(checkNames);
         Assertions.assertEquals(3, schema.getAllEntries().map(Entry::getName).distinct().count());
 
-        final Entry entry3Bis = this.newEntry("name_b_1", "c_value");
+        final Entry entry3Bis = newEntry("name_b_1", "c_value");
 
-        final Schema schemaBis = this.newSchema(entry1, entry2, entry3Bis);
+        final Schema schemaBis = newSchema(entry1, entry2, entry3Bis);
         final boolean checkNamesBis = schemaBis
                 .getAllEntries()
                 .allMatch((Entry e) -> ("1name_b".equals(e.getRawName()) && e.getName().matches("name_b(_2)?")
@@ -229,10 +250,10 @@ void testAntiCollision() {
 
         final Schema.Builder builder = new BuilderImpl().withType(Type.RECORD);
         for (int index = 1; index < 8; index++) {
-            final Entry e = this.newEntry(index + "name_b", index + "_value");
+            final Entry e = newEntry(index + "name_b", index + "_value");
             builder.withEntry(e);
         }
-        final Entry last = this.newEntry("name_b_5", "last_value");
+        final Entry last = newEntry("name_b_5", "last_value");
         builder.withEntry(last);
         final Schema schemaTer = builder.build();
         Assertions.assertEquals(8, schemaTer.getAllEntries().map(Entry::getName).distinct().count());
@@ -241,7 +262,7 @@ void testAntiCollision() {
                         schemaTer
                                 .getAllEntries()
                                 .map(Entry::getName)
-                                .filter((String name) -> "name_b".equals(name))
+                                .filter("name_b"::equals)
                                 .count());
         Assertions
                 .assertEquals(7,
@@ -258,10 +279,10 @@ void testAntiCollision() {
                 .withType(Type.LONG) //
                 .withDefaultValue(0L) //
                 .build();
-        Assertions.assertThrows(IllegalArgumentException.class, () -> this.newSchema(entry3, entry3Twin));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> newSchema(entry3, entry3Twin));
     }
 
-    private Schema newSchema(Entry... entries) {
+    private static Schema newSchema(Entry... entries) {
         final Schema.Builder builder = new BuilderImpl().withType(Type.RECORD);
         for (Entry e : entries) {
             builder.withEntry(e);
@@ -269,7 +290,7 @@ private Schema newSchema(Entry... entries) {
         return builder.build();
     }
 
-    private Entry newEntry(final String name, final String defaultValue) {
+    private static Entry newEntry(final String name, final String defaultValue) {
         return new EntryImpl.BuilderImpl() //
                 .withName(name) //
                 .withType(Type.STRING) //
@@ -410,39 +431,6 @@ void testToBuilder() {
         assertEquals("meta1,data1,meta2,data2,data3,meta3", getSchemaFields(schemaNew));
     }
 
-    @Test
-    void testAvoidCollision() {
-        final Map entries = new HashMap<>();
-        for (int index = 1; index < 8; index++) {
-            final Schema.Entry e = this.newEntry(index + "name_b", Type.STRING);
-            final Schema.Entry realEntry = Schema.avoidCollision(e, entries::get, entries::put);
-            entries.put(realEntry.getName(), realEntry);
-        }
-        final Entry last = this.newEntry("name_b_5", Type.STRING);
-        final Schema.Entry realEntry = Schema.avoidCollision(last, entries::get, entries::put);
-        entries.put(realEntry.getName(), realEntry);
-
-        Assertions.assertEquals(8, entries.size());
-        Assertions.assertEquals("name_b", entries.get("name_b").getName());
-        Assertions
-                .assertTrue(IntStream
-                        .range(1, 8)
-                        .mapToObj((int i) -> "name_b_" + i)
-                        .allMatch((String name) -> entries.get(name).getName().equals(name)));
-
-        final Map entriesDuplicate = new HashMap<>();
-        final Schema.Entry e1 = this.newEntry("goodName", Type.STRING);
-        final Schema.Entry realEntry1 =
-                Schema.avoidCollision(e1, entriesDuplicate::get, entriesDuplicate::put);
-        Assertions.assertSame(e1, realEntry1);
-        entriesDuplicate.put(realEntry1.getName(), realEntry1);
-        final Schema.Entry e2 = this.newEntry("goodName", Type.STRING);
-        final Schema.Entry realEntry2 =
-                Schema.avoidCollision(e2, entriesDuplicate::get, entriesDuplicate::put);
-
-        Assertions.assertSame(realEntry2, e2);
-    }
-
     @RepeatedTest(20)
     void entriesOrderShouldBeDeterministic() {
         final List entries = IntStream
diff --git a/component-studio/component-runtime-di/pom.xml b/component-studio/component-runtime-di/pom.xml
index 935b956297353..080deae81092f 100644
--- a/component-studio/component-runtime-di/pom.xml
+++ b/component-studio/component-runtime-di/pom.xml
@@ -69,7 +69,8 @@
       component-runtime-manager
       ${project.version}
     
-    
+    
+      
       org.talend.sdk.component
       component-runtime-design-extension
       ${project.version}
diff --git a/component-studio/component-runtime-di/src/main/java/org/talend/sdk/component/runtime/di/record/DiRowStructVisitor.java b/component-studio/component-runtime-di/src/main/java/org/talend/sdk/component/runtime/di/record/DiRowStructVisitor.java
index 942bfc8f7fd09..a954ece0fd18d 100644
--- a/component-studio/component-runtime-di/src/main/java/org/talend/sdk/component/runtime/di/record/DiRowStructVisitor.java
+++ b/component-studio/component-runtime-di/src/main/java/org/talend/sdk/component/runtime/di/record/DiRowStructVisitor.java
@@ -29,7 +29,7 @@
 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.talend.sdk.component.api.record.Schema.sanitizeConnectionName;
+import static org.talend.sdk.component.api.record.SchemaCompanionUtil.sanitizeName;
 import static org.talend.sdk.component.api.record.SchemaProperty.IS_KEY;
 import static org.talend.sdk.component.api.record.SchemaProperty.PATTERN;
 import static org.talend.sdk.component.api.record.SchemaProperty.SCALE;
@@ -166,7 +166,7 @@ private void handleDynamic(final Object raw) {
         final DynamicWrapper dynamic = new DynamicWrapper(raw);
         dynamic.getDynamic().metadatas.forEach(meta -> {
             final Object value = dynamic.getDynamic().getColumnValue(meta.getName());
-            final String metaName = sanitizeConnectionName(meta.getName());
+            final String metaName = sanitizeName(meta.getName());
             final String metaOriginalName = meta.getDbName();
             log.trace("[visit] Dynamic {}\t({})\t ==> {}.", meta.getName(), meta.getType(), value);
             if (value == null) {
@@ -280,7 +280,7 @@ private Schema inferSchema(final Object data, final RecordBuilderFactory factory
                     log.trace("[inferSchema] Skipping technical field {}.", field.getName());
                     return;
                 }
-                final String name = sanitizeConnectionName(field.getName());
+                final String name = sanitizeName(field.getName());
                 final Object raw = field.get(data);
                 final boolean isNullable =
                         ofNullable(getMetadata(name + "IsNullable", data, Boolean.class)).orElse(true);
@@ -348,7 +348,7 @@ private Schema inferSchema(final Object data, final RecordBuilderFactory factory
                         final DynamicWrapper dynamic = new DynamicWrapper(raw);
                         dynamic.getDynamic().metadatas.forEach(meta -> {
                             final Object value = dynamic.getDynamic().getColumnValue(meta.getName());
-                            final String metaName = sanitizeConnectionName(meta.getName());
+                            final String metaName = sanitizeName(meta.getName());
                             final String metaOriginalName = meta.getDbName();
                             final boolean metaIsNullable = meta.isNullable();
                             final boolean metaIsKey = meta.isKey();
diff --git a/component-studio/pom.xml b/component-studio/pom.xml
index 44f6030ef63c5..1c8f1c57020e2 100644
--- a/component-studio/pom.xml
+++ b/component-studio/pom.xml
@@ -59,7 +59,8 @@
         ${project.version}
       
 
-      
+      
+        
         org.apache.tomcat
         tomcat-api
         ${tomcat.version}
diff --git a/component-tools/pom.xml b/component-tools/pom.xml
index d53620de46229..aa3e14a3c6a23 100644
--- a/component-tools/pom.xml
+++ b/component-tools/pom.xml
@@ -37,7 +37,8 @@
       component-runtime-manager
       ${project.version}
     
-    
+    
+      
       org.jruby
       jruby-complete
       ${jruby.version}
@@ -84,7 +85,8 @@
       
     
 
-    
+    
+      
       xml-apis
       xml-apis
       1.0.b2
@@ -153,7 +155,8 @@
 
   
     
-      
+      
+        
         com.github.eirslett
         frontend-maven-plugin
         ${frontend.version}
@@ -185,7 +188,8 @@
           
         
       
-      
+      
+        
         org.codehaus.gmavenplus
         gmavenplus-plugin
         
diff --git a/component-tools/src/main/java/org/talend/sdk/component/tools/ComponentValidator.java b/component-tools/src/main/java/org/talend/sdk/component/tools/ComponentValidator.java
index 9d6168c74c440..6abdb1a12a051 100755
--- a/component-tools/src/main/java/org/talend/sdk/component/tools/ComponentValidator.java
+++ b/component-tools/src/main/java/org/talend/sdk/component/tools/ComponentValidator.java
@@ -35,7 +35,6 @@
 import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.function.Function;
 import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
diff --git a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/ActionValidatorTest.java b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/ActionValidatorTest.java
index a47cefb630ee8..09cef26fc31b4 100644
--- a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/ActionValidatorTest.java
+++ b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/ActionValidatorTest.java
@@ -83,7 +83,8 @@ void validateDiscoverProcessorSchema() {
                 validator.validate(finder, Collections.singletonList(ActionDiscoverProcessorSchemaOk.class));
         assertEquals(0, noerrors.count());
         finder = new AnnotationFinder(new ClassesArchive(ActionDiscoverProcessorSchemaKo.class));
-        final Stream errors = validator.validate(finder, Collections.singletonList(ActionDiscoverProcessorSchemaKo.class));
+        final Stream errors =
+                validator.validate(finder, Collections.singletonList(ActionDiscoverProcessorSchemaKo.class));
         assertEquals(13, errors.count());
     }
 
@@ -96,7 +97,8 @@ void validateDynamicDependencies() {
         assertEquals(0, noerrors.count());
 
         finder = new AnnotationFinder(new ClassesArchive(ActionDynamicDependenciesKO.class));
-        final Stream errors = validator.validate(finder, Collections.singletonList(ActionDynamicDependenciesKO.class));
+        final Stream errors =
+                validator.validate(finder, Collections.singletonList(ActionDynamicDependenciesKO.class));
         assertEquals(9, errors.count());
     }
 
@@ -112,7 +114,8 @@ void validate() {
         Assertions.assertTrue(errors.isEmpty(), () -> errors.get(0) + " as first error");
 
         AnnotationFinder finderKO = new AnnotationFinder(new ClassesArchive(ActionClassKO.class));
-        final Stream errorsStreamKO = validator.validate(finderKO, Collections.singletonList(ActionClassKO.class));
+        final Stream errorsStreamKO =
+                validator.validate(finderKO, Collections.singletonList(ActionClassKO.class));
         final List errorsKO = errorsStreamKO.collect(Collectors.toList());
         assertEquals(6, errorsKO.size(), () -> errorsKO.get(0) + " as first error");
 
@@ -136,7 +139,8 @@ void validateAvailableOutputFlows() {
         assertEquals(0, noerrors.count());
 
         finder = new AnnotationFinder(new ClassesArchive(AvailableOutputFlowsKO.class));
-        final Stream errors = validator.validate(finder, Collections.singletonList(AvailableOutputFlowsKO.class));
+        final Stream errors =
+                validator.validate(finder, Collections.singletonList(AvailableOutputFlowsKO.class));
         assertEquals(3, errors.count());
     }
 
diff --git a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/HttpClientValidatorTest.java b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/HttpClientValidatorTest.java
index 4b03cc3d4970c..35f607515366c 100644
--- a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/HttpClientValidatorTest.java
+++ b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/HttpClientValidatorTest.java
@@ -17,7 +17,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Stream;
diff --git a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/OutputConnectionValidatorTest.java b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/OutputConnectionValidatorTest.java
index 5346510be5b99..52eca7ac611be 100644
--- a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/OutputConnectionValidatorTest.java
+++ b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/OutputConnectionValidatorTest.java
@@ -15,7 +15,6 @@
  */
 package org.talend.sdk.component.tools.validator;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.stream.Stream;
 
diff --git a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/RecordValidatorTest.java b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/RecordValidatorTest.java
index 3182715aa58b7..44d0dfb42f477 100644
--- a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/RecordValidatorTest.java
+++ b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/RecordValidatorTest.java
@@ -17,7 +17,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.stream.Stream;
 
diff --git a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/SchemaValidatorTest.java b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/SchemaValidatorTest.java
index 5ddf10a451737..214cb27bf1596 100644
--- a/component-tools/src/test/java/org/talend/sdk/component/tools/validator/SchemaValidatorTest.java
+++ b/component-tools/src/test/java/org/talend/sdk/component/tools/validator/SchemaValidatorTest.java
@@ -17,7 +17,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
diff --git a/sample-parent/sample-connector/pom.xml b/sample-parent/sample-connector/pom.xml
index 0ff7036eb29f8..f88f76114ff3f 100644
--- a/sample-parent/sample-connector/pom.xml
+++ b/sample-parent/sample-connector/pom.xml
@@ -13,7 +13,8 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-
+
+  
   4.0.0
 
   
diff --git a/sample-parent/sample/pom.xml b/sample-parent/sample/pom.xml
index 554ec91fd8bbd..1e3e7944ef81b 100644
--- a/sample-parent/sample/pom.xml
+++ b/sample-parent/sample/pom.xml
@@ -32,12 +32,14 @@
   
 
   
-    
+    
+      
       org.apache.commons
       commons-lang3
     
 
-    
+    
+      
       org.talend.sdk.component
       component-runtime-manager
       ${project.version}
diff --git a/talend-component-maven-plugin/pom.xml b/talend-component-maven-plugin/pom.xml
index 8e3b52791ced8..1bba35c5e3919 100644
--- a/talend-component-maven-plugin/pom.xml
+++ b/talend-component-maven-plugin/pom.xml
@@ -50,7 +50,8 @@
       component-tools
       ${project.version}
     
-    
+    
+      
       org.talend.sdk.component
       component-runtime-design-extension
       ${project.version}