001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.record; 017 018import static java.util.Collections.unmodifiableList; 019import static java.util.stream.Collectors.joining; 020import static java.util.stream.Collectors.toList; 021import static java.util.stream.Collectors.toMap; 022 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Objects; 031import java.util.stream.Stream; 032 033import javax.json.bind.annotation.JsonbTransient; 034 035import org.talend.sdk.component.api.record.OrderedMap; 036import org.talend.sdk.component.api.record.Schema; 037import org.talend.sdk.component.api.record.SchemaProperty; 038 039import lombok.EqualsAndHashCode; 040import lombok.Getter; 041import lombok.ToString; 042 043@ToString 044public class SchemaImpl implements Schema { 045 046 @Getter 047 private final Type type; 048 049 @Getter 050 private final Schema elementSchema; 051 052 @Getter 053 private final List<Entry> entries; 054 055 @JsonbTransient 056 private final List<Entry> metadataEntries; 057 058 @Getter 059 private final Map<String, String> props; 060 061 @JsonbTransient 062 private final EntriesOrder entriesOrder; 063 064 @Getter 065 @JsonbTransient 066 private Map<String, Entry> entryMap = new HashMap<>(); 067 068 public static final String ENTRIES_ORDER_PROP = "talend.fields.order"; 069 070 SchemaImpl(final SchemaImpl.BuilderImpl builder) { 071 this.type = builder.type; 072 this.elementSchema = builder.elementSchema; 073 this.entries = unmodifiableList(builder.entries.streams().collect(toList())); 074 this.metadataEntries = unmodifiableList(builder.metadataEntries.streams().collect(toList())); 075 this.props = builder.props; 076 entriesOrder = EntriesOrder.of(getFieldsOrder()); 077 getAllEntries().forEach(e -> entryMap.put(e.getName(), e)); 078 } 079 080 /** 081 * Optimized hashcode method (do not enter inside field hashcode, just getName, ignore props fields). 082 * 083 * @return hashcode. 084 */ 085 @Override 086 public int hashCode() { 087 final String e1 = 088 this.entries != null ? this.entries.stream().map(Entry::getName).collect(joining(",")) : ""; 089 final String m1 = this.metadataEntries != null 090 ? this.metadataEntries.stream().map(Entry::getName).collect(joining(",")) 091 : ""; 092 093 return Objects.hash(this.type, this.elementSchema, e1, m1); 094 } 095 096 @Override 097 public boolean equals(final Object obj) { 098 if (obj == this) { 099 return true; 100 } 101 if (!(obj instanceof SchemaImpl)) { 102 return false; 103 } 104 final SchemaImpl other = (SchemaImpl) obj; 105 if (!other.canEqual(this)) { 106 return false; 107 } 108 return Objects.equals(this.type, other.type) 109 && Objects.equals(this.elementSchema, other.elementSchema) 110 && Objects.equals(this.entries, other.entries) 111 && Objects.equals(this.metadataEntries, other.metadataEntries) 112 && Objects.equals(this.props, other.props); 113 } 114 115 protected boolean canEqual(final SchemaImpl other) { 116 return true; 117 } 118 119 @Override 120 public String getProp(final String property) { 121 return props.get(property); 122 } 123 124 @Override 125 public List<Entry> getMetadata() { 126 return this.metadataEntries; 127 } 128 129 @Override 130 @JsonbTransient 131 public Stream<Entry> getAllEntries() { 132 return Stream.concat(this.metadataEntries.stream(), this.entries.stream()); 133 } 134 135 @Override 136 public Builder toBuilder() { 137 final Builder builder = new BuilderImpl() 138 .withType(this.type) 139 .withElementSchema(this.elementSchema) 140 .withProps(this.props 141 .entrySet() 142 .stream() 143 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))); 144 getEntriesOrdered().forEach(builder::withEntry); 145 return builder; 146 } 147 148 @Override 149 @JsonbTransient 150 public List<Entry> getEntriesOrdered() { 151 return getAllEntries().sorted(entriesOrder).collect(toList()); 152 } 153 154 @Override 155 @JsonbTransient 156 public EntriesOrder naturalOrder() { 157 return entriesOrder; 158 } 159 160 private String getFieldsOrder() { 161 String fields = getProp(ENTRIES_ORDER_PROP); 162 if (fields == null || fields.isEmpty()) { 163 fields = getAllEntries().map(Entry::getName).collect(joining(",")); 164 props.put(ENTRIES_ORDER_PROP, fields); 165 } 166 return fields; 167 } 168 169 public static class BuilderImpl implements Builder { 170 171 private Type type; 172 173 private Schema elementSchema; 174 175 private final OrderedMap<Schema.Entry> entries = new OrderedMap<>(Schema.Entry::getName); 176 177 private final OrderedMap<Schema.Entry> metadataEntries = new OrderedMap<>(Schema.Entry::getName); 178 179 private Map<String, String> props = new LinkedHashMap<>(0); 180 181 private List<String> entriesOrder = new ArrayList<>(); 182 183 @Override 184 public Builder withElementSchema(final Schema schema) { 185 if (type != Type.ARRAY && schema != null) { 186 throw new IllegalArgumentException("elementSchema is only valid for ARRAY type of schema"); 187 } 188 elementSchema = schema; 189 return this; 190 } 191 192 @Override 193 public Builder withType(final Type type) { 194 this.type = type; 195 return this; 196 } 197 198 @Override 199 public Builder withEntry(final Entry entry) { 200 if (type != Type.RECORD) { 201 throw new IllegalArgumentException("entry is only valid for RECORD type of schema"); 202 } 203 final Entry entryToAdd = Schema.avoidCollision(entry, 204 this::getEntry, 205 this::replaceEntry); 206 if (entryToAdd == null) { 207 // mean try to add entry with same name. 208 throw new IllegalArgumentException("Entry with name " + entry.getName() + " already exist in schema"); 209 } 210 if (entry.isMetadata()) { 211 this.metadataEntries.addValue(entryToAdd); 212 } else { 213 this.entries.addValue(entryToAdd); 214 } 215 216 entriesOrder.add(entry.getName()); 217 return this; 218 } 219 220 @Override 221 public Builder withEntryAfter(final String after, final Entry entry) { 222 withEntry(entry); 223 return moveAfter(after, entry.getName()); 224 } 225 226 @Override 227 public Builder withEntryBefore(final String before, final Entry entry) { 228 withEntry(entry); 229 return moveBefore(before, entry.getName()); 230 } 231 232 private void replaceEntry(final String name, final Schema.Entry entry) { 233 if (this.entries.getValue(entry.getName()) != null) { 234 this.entries.replace(name, entry); 235 } else if (this.metadataEntries.getValue(name) != null) { 236 this.metadataEntries.replace(name, entry); 237 } 238 } 239 240 private Stream<Entry> getAllEntries() { 241 return Stream.concat(this.entries.streams(), this.metadataEntries.streams()); 242 } 243 244 @Override 245 public Builder withProp(final String key, final String value) { 246 props.put(key, value); 247 return this; 248 } 249 250 @Override 251 public Builder withProps(final Map<String, String> props) { 252 if (props != null) { 253 this.props = props; 254 } 255 return this; 256 } 257 258 @Override 259 public Builder remove(final String name) { 260 final Entry entry = this.getEntry(name); 261 if (entry == null) { 262 throw new IllegalArgumentException(String.format("%s not in schema", name)); 263 } 264 return this.remove(entry); 265 } 266 267 @Override 268 public Builder remove(final Entry entry) { 269 if (entry != null) { 270 if (entry.isMetadata()) { 271 if (this.metadataEntries.getValue(entry.getName()) != null) { 272 metadataEntries.removeValue(entry); 273 } 274 } else if (this.entries.getValue(entry.getName()) != null) { 275 entries.removeValue(entry); 276 } 277 entriesOrder.remove(entry.getName()); 278 } 279 return this; 280 } 281 282 @Override 283 public Builder moveAfter(final String after, final String name) { 284 if (entriesOrder.indexOf(after) == -1) { 285 throw new IllegalArgumentException(String.format("%s not in schema", after)); 286 } 287 entriesOrder.remove(name); 288 int destination = entriesOrder.indexOf(after) + 1; 289 if (destination < entriesOrder.size()) { 290 entriesOrder.add(destination, name); 291 } else { 292 entriesOrder.add(name); 293 } 294 return this; 295 } 296 297 @Override 298 public Builder moveBefore(final String before, final String name) { 299 if (entriesOrder.indexOf(before) == -1) { 300 throw new IllegalArgumentException(String.format("%s not in schema", before)); 301 } 302 entriesOrder.remove(name); 303 entriesOrder.add(entriesOrder.indexOf(before), name); 304 return this; 305 } 306 307 @Override 308 public Builder swap(final String name, final String with) { 309 Collections.swap(entriesOrder, entriesOrder.indexOf(name), entriesOrder.indexOf(with)); 310 return this; 311 } 312 313 @Override 314 public Schema build() { 315 if (this.entriesOrder != null && !this.entriesOrder.isEmpty()) { 316 this.props.put(ENTRIES_ORDER_PROP, entriesOrder.stream().collect(joining(","))); 317 } 318 return new SchemaImpl(this); 319 } 320 321 @Override 322 public Schema build(final Comparator<Entry> order) { 323 final String entriesOrderProp = 324 this.getAllEntries().sorted(order).map(Entry::getName).collect(joining(",")); 325 this.props.put(ENTRIES_ORDER_PROP, entriesOrderProp); 326 327 return new SchemaImpl(this); 328 } 329 330 private Schema.Entry getEntry(final String name) { 331 Entry entry = this.entries.getValue(name); 332 if (entry == null) { 333 entry = this.metadataEntries.getValue(name); 334 } 335 return entry; 336 } 337 } 338 339 /** 340 * {@link org.talend.sdk.component.api.record.Schema.Entry} implementation. 341 */ 342 @EqualsAndHashCode 343 @ToString 344 public static class EntryImpl implements org.talend.sdk.component.api.record.Schema.Entry { 345 346 private EntryImpl(final EntryImpl.BuilderImpl builder) { 347 this.name = builder.name; 348 this.rawName = builder.rawName; 349 if (builder.type == null && builder.logicalType != null) { 350 this.type = builder.logicalType.storageType(); 351 } else { 352 this.type = builder.type; 353 } 354 this.nullable = builder.nullable; 355 this.metadata = builder.metadata; 356 this.defaultValue = builder.defaultValue; 357 this.elementSchema = builder.elementSchema; 358 this.comment = builder.comment; 359 this.props.putAll(builder.props); 360 } 361 362 /** 363 * The name of this entry. 364 */ 365 private final String name; 366 367 /** 368 * The raw name of this entry. 369 */ 370 private final String rawName; 371 372 /** 373 * Type of the entry, this determine which other fields are populated. 374 */ 375 private final Schema.Type type; 376 377 /** 378 * Is this entry nullable or always valued. 379 */ 380 private final boolean nullable; 381 382 /** 383 * Is this entry a metadata entry. 384 */ 385 private final boolean metadata; 386 387 /** 388 * Default value for this entry. 389 */ 390 private final Object defaultValue; 391 392 /** 393 * For type == record, the element type. 394 */ 395 private final Schema elementSchema; 396 397 /** 398 * Allows to associate to this field a comment - for doc purposes, no use in the runtime. 399 */ 400 private final String comment; 401 402 /** 403 * metadata 404 */ 405 private final Map<String, String> props = new LinkedHashMap<>(0); 406 407 @Override 408 @JsonbTransient 409 public String getOriginalFieldName() { 410 return rawName != null ? rawName : name; 411 } 412 413 @Override 414 public String getProp(final String property) { 415 return this.props.get(property); 416 } 417 418 @Override 419 public Entry.Builder toBuilder() { 420 return new EntryImpl.BuilderImpl(this); 421 } 422 423 @Override 424 public String getName() { 425 return this.name; 426 } 427 428 @Override 429 public String getRawName() { 430 return this.rawName; 431 } 432 433 @Override 434 public Type getType() { 435 return this.type; 436 } 437 438 @Override 439 public boolean isNullable() { 440 return this.nullable; 441 } 442 443 @Override 444 public boolean isMetadata() { 445 return this.metadata; 446 } 447 448 @Override 449 public Object getDefaultValue() { 450 return this.defaultValue; 451 } 452 453 @Override 454 public Schema getElementSchema() { 455 return this.elementSchema; 456 } 457 458 @Override 459 public String getComment() { 460 return this.comment; 461 } 462 463 @Override 464 public Map<String, String> getProps() { 465 return this.props; 466 } 467 468 /** 469 * Plain builder matching {@link Entry} structure. 470 */ 471 public static class BuilderImpl implements Entry.Builder { 472 473 private String name; 474 475 private String rawName; 476 477 private Schema.Type type; 478 479 private boolean nullable; 480 481 private boolean metadata = false; 482 483 private Object defaultValue; 484 485 private Schema elementSchema; 486 487 private String comment; 488 489 private SchemaProperty.LogicalType logicalType; 490 491 private final Map<String, String> props = new LinkedHashMap<>(0); 492 493 public BuilderImpl() { 494 } 495 496 private BuilderImpl(final Entry entry) { 497 this.name = entry.getName(); 498 this.rawName = entry.getRawName(); 499 this.nullable = entry.isNullable(); 500 this.type = entry.getType(); 501 this.comment = entry.getComment(); 502 this.elementSchema = entry.getElementSchema(); 503 this.defaultValue = entry.getDefaultValue(); 504 this.metadata = entry.isMetadata(); 505 this.props.putAll(entry.getProps()); 506 } 507 508 public Builder withName(final String name) { 509 this.name = Schema.sanitizeConnectionName(name); 510 // if raw name is changed as follow name rule, use label to store raw name 511 // if not changed, not set label to save space 512 if (!name.equals(this.name)) { 513 this.rawName = name; 514 } 515 return this; 516 } 517 518 @Override 519 public Builder withRawName(final String rawName) { 520 this.rawName = rawName; 521 return this; 522 } 523 524 @Override 525 public Builder withType(final Type type) { 526 this.type = type; 527 return this; 528 } 529 530 @Override 531 public Builder withLogicalType(final SchemaProperty.LogicalType logicalType) { 532 this.logicalType = logicalType; 533 this.props.put(SchemaProperty.LOGICAL_TYPE, logicalType.key()); 534 return this; 535 } 536 537 @Override 538 public Builder withNullable(final boolean nullable) { 539 this.nullable = nullable; 540 return this; 541 } 542 543 @Override 544 public Builder withMetadata(final boolean metadata) { 545 this.metadata = metadata; 546 return this; 547 } 548 549 @Override 550 public <T> Builder withDefaultValue(final T value) { 551 defaultValue = value; 552 return this; 553 } 554 555 @Override 556 public Builder withElementSchema(final Schema schema) { 557 elementSchema = schema; 558 return this; 559 } 560 561 @Override 562 public Builder withComment(final String comment) { 563 this.comment = comment; 564 return this; 565 } 566 567 @Override 568 public Builder withProp(final String key, final String value) { 569 props.put(key, value); 570 return this; 571 } 572 573 @Override 574 public Builder withProps(final Map props) { 575 if (props == null) { 576 return this; 577 } 578 this.props.putAll(props); 579 return this; 580 } 581 582 public Entry build() { 583 return new EntryImpl(this); 584 } 585 586 } 587 } 588 589}