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